Skip to contentSkip to conversation

bitmap: integrate a lookup table extension to the bitmap format

PATCH Superseded Abhradeep Chakraborty via GitGitGadget 11 participants 162 messages Jun 20, 2022 – Aug 26, 2022 Documentation Page 1 of 2

lore
Share & tools
lore
#1 lore raw

PATCH 1/6 Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit oids, along with their offset and xor offset. This way git can load only the neccesary bitmaps without loading the previous bitmaps.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

---
 Documentation/technical/bitmap-format.txt | 31 +++++++++++++++++++++++
 1 file changed, 31 insertions(+)
Documentation/technical/bitmap-format.txt +31 -0
index 04b3ec21785..34e98787b78 100644
@@ -67,6 +67,14 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0xf) : :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` object ids, a list of pairs of
+			offset and xor offset of respective objects, and 4-byte
+			integer denoting the flags (currently none). The format
+			and meaning of the table is described below.
+
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +213,26 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the end of the `.bitmap`
+contains a lookup table specifying the positions of commits which have a
+bitmap.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the format
+is as follows:
+
+	- `nr_entries` object names.
+
+	- `nr_entries` pairs of 4-byte integers, each in network order.
+	  The first holds the offset from which that commit's bitmap can
+	  be read. The second number holds the position of the commit
+	  whose bitmap the current bitmap is xor'd with in lexicographic
+	  order, or 0xffffffff if the current commit is not xor'd with
+	  anything.
+
+	- One 4-byte network byte order integer specifying
+	  table-specific flags. None exist currently, so this is always
+	  "0".
Signature · 1 line
gitgitgadget
#0 lore raw

cover bitmap: integrate a lookup table extension to the bitmap format

When parsing the .bitmap file, git loads all the bitmaps one by one even if some of the bitmaps are not necessary. We can remove this overhead by loading only the necessary bitmaps. A look up table extension can solve this issue.

The proposed table has:

* a list of nr_entries object ids. These objects are commits that has bitmaps. Ids are stored in lexicographic order (for better searching).
* a list of <offset, xor-offset> pairs (4-byte integers, network-byte order). The i'th pair denotes the offset and xor-offset(respectively) of the bitmap of i'th commit in the previous list. These two informations are necessary because only in this way bitmaps can be found without parsing all the bitmap.
* a 4-byte integer for table specific flags (none exists currently).

Whenever git want to parse the bitmap for a specific commit, it will first refer to the table and will look for the offset and xor-offset for that commit. Git will then try to parse the bitmap located at the offset position. The xor-offset can be used to find the xor-bitmap for the bitmap(if any). This process is recursive and will end if xor-offset is null (i.e. there is no xor-bitmap left).

Abhradeep Chakraborty (5):
Documentation/technical: describe bitmap lookup table extension
pack-bitmap: prepare to read lookup table extension pack-bitmap-write.c: write lookup table extension
bitmap-commit-table: add tests for the bitmap lookup table
bitmap-lookup-table: add performance tests

Taylor Blau (1):
builtin/pack-objects.c: learn pack.writeBitmapLookupTable

 Documentation/config/pack.txt             |   7 +
 Documentation/technical/bitmap-format.txt |  31 ++++
 builtin/pack-objects.c                    |   8 +
 pack-bitmap-write.c                       |  59 +++++++-
 pack-bitmap.c                             | 172 +++++++++++++++++++++-
 pack-bitmap.h                             |   1 +
 t/perf/p5310-pack-bitmaps.sh              |  60 +++++---
 t/perf/p5326-multi-pack-bitmaps.sh        |  55 ++++---
 t/t5310-pack-bitmaps.sh                   |  14 ++
 t/t5326-multi-pack-bitmaps.sh             |  19 +++
 10 files changed, 375 insertions(+), 51 deletions(-)

base-commit: 5699ec1b0aec51b9e9ba5a2785f65970c5a95d84
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1266%2FAbhra303%2Fbitmap-commit-table-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1266/Abhra303/bitmap-commit-table-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1266

Signature · 1 line
gitgitgadget
#14 lore raw

PATCH 2/6 pack-bitmap: prepare to read lookup table extension

From: Abhradeep Chakraborty <[email protected]>

Bitmap lookup table extension can let git to parse only the necessary bitmaps without loading the previous bitmaps one by one.

Teach git to read and use the bitmap lookup table extension.

---
 pack-bitmap.c | 172 ++++++++++++++++++++++++++++++++++++++++++++++++--
 pack-bitmap.h |   1 +
 2 files changed, 166 insertions(+), 7 deletions(-)
pack-bitmap.c +165 -7
index 36134222d7a..d5e5973a79f 100644
@@ -15,6 +15,7 @@
 #include "list-objects-filter-options.h"
 #include "midx.h"
 #include "config.h"
+#include "hash-lookup.h"
 
 /*
  * An entry on the bitmap index, representing the bitmap for a given
@@ -82,6 +83,13 @@ struct bitmap_index {
 	/* The checksum of the packfile or MIDX; points into map. */
 	const unsigned char *checksum;
 
+	/*
+	 * If not NULL, these point into the various commit table sections
+	 * (within map).
+	 */
+	unsigned char *table_lookup;
+	unsigned char *table_offsets;
+
 	/*
 	 * Extended index.
 	 *
@@ -185,6 +193,24 @@ static int load_bitmap_header(struct bitmap_index *index)
 			index->hashes = (void *)(index_end - cache_size);
 			index_end -= cache_size;
 		}
+
+		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
+		    git_env_bool("GIT_READ_COMMIT_TABLE", 1)) {
+			uint32_t entry_count = ntohl(header->entry_count);
+			uint32_t table_size =
+				(entry_count * the_hash_algo->rawsz) /* oids */ +
+				(entry_count * sizeof(uint32_t)) /* offsets */ +
+				(entry_count * sizeof(uint32_t)) /* xor offsets */ +
+				(sizeof(uint32_t)) /* flags */;
+
+			if (table_size > index_end - index->map - header_size)
+				return error("corrupted bitmap index file (too short to fit commit table)");
+
+			index->table_lookup = (void *)(index_end - table_size);
+			index->table_offsets = index->table_lookup + the_hash_algo->rawsz * entry_count;
+
+			index_end -= table_size;
+		}
 	}
 
 	index->entry_count = ntohl(header->entry_count);
@@ -470,7 +496,7 @@ static int load_bitmap(struct bitmap_index *bitmap_git)
 		!(bitmap_git->tags = read_bitmap_1(bitmap_git)))
 		goto failed;
 
-	if (load_bitmap_entries_v1(bitmap_git) < 0)
+	if (!bitmap_git->table_lookup && load_bitmap_entries_v1(bitmap_git) < 0)
 		goto failed;
 
 	return 0;
@@ -557,14 +583,145 @@ struct include_data {
 	struct bitmap *seen;
 };
 
-struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
-				      struct commit *commit)
+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						      struct commit *commit,
+						      uint32_t *pos_hint);
+
+static inline const unsigned char *bitmap_oid_pos(struct bitmap_index *bitmap_git,
+						  uint32_t pos)
+{
+	return bitmap_git->table_lookup + (pos * the_hash_algo->rawsz);
+}
+
+static inline const void *bitmap_offset_pos(struct bitmap_index *bitmap_git,
+					    uint32_t pos)
+{
+	return bitmap_git->table_offsets + (pos * 2 * sizeof(uint32_t));
+}
+
+static inline const void *xor_position_pos(struct bitmap_index *bitmap_git,
+					   uint32_t pos)
+{
+	return (unsigned char*) bitmap_offset_pos(bitmap_git, pos) + sizeof(uint32_t);
+}
+
+static int bitmap_lookup_cmp(const void *_va, const void *_vb)
+{
+	return hashcmp(_va, _vb);
+}
+
+static int bitmap_table_lookup(struct bitmap_index *bitmap_git,
+			       struct object_id *oid,
+			       uint32_t *commit_pos)
+{
+	unsigned char *found = bsearch(oid->hash, bitmap_git->table_lookup,
+				       bitmap_git->entry_count,
+				       the_hash_algo->rawsz, bitmap_lookup_cmp);
+	if (found)
+		*commit_pos = (found - bitmap_git->table_lookup) / the_hash_algo->rawsz;
+	return !!found;
+}
+
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						    struct object_id *oid,
+						    uint32_t commit_pos)
+{
+	uint32_t xor_pos;
+	off_t bitmap_ofs;
+
+	int flags;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap;
+
+	bitmap_ofs = get_be32(bitmap_offset_pos(bitmap_git, commit_pos));
+	xor_pos = get_be32(xor_position_pos(bitmap_git, commit_pos));
+
+	/*
+	 * Lazily load the xor'd bitmap if required (and we haven't done so
+	 * already). Make sure to pass the xor'd bitmap's position along as a
+	 * hint to avoid an unnecessary binary search in
+	 * stored_bitmap_for_commit().
+	 */
+	if (xor_pos == 0xffffffff) {
+		xor_bitmap = NULL;
+	} else {
+		struct commit *xor_commit;
+		struct object_id xor_oid;
+
+		oidread(&xor_oid, bitmap_oid_pos(bitmap_git, xor_pos));
+
+		xor_commit = lookup_commit(the_repository, &xor_oid);
+		if (!xor_commit)
+			return NULL;
+
+		xor_bitmap = stored_bitmap_for_commit(bitmap_git, xor_commit,
+						      &xor_pos);
+	}
+
+	/*
+	 * Don't bother reading the commit's index position or its xor
+	 * offset:
+	 *
+	 *   - The commit's index position is irrelevant to us, since
+	 *     load_bitmap_entries_v1 only uses it to learn the object
+	 *     id which is used to compute the hashmap's key. We already
+	 *     have an object id, so no need to look it up again.
+	 *
+	 *   - The xor_offset is unusable for us, since it specifies how
+	 *     many entries previous to ours we should look at. This
+	 *     makes sense when reading the bitmaps sequentially (as in
+	 *     load_bitmap_entries_v1()), since we can keep track of
+	 *     each bitmap as we read them.
+	 *
+	 *     But it can't work for us, since the bitmap's don't have a
+	 *     fixed size. So we learn the position of the xor'd bitmap
+	 *     from the commit table (and resolve it to a bitmap in the
+	 *     above if-statement).
+	 *
+	 * Instead, we can skip ahead and immediately read the flags and
+	 * ewah bitmap.
+	 */
+	bitmap_git->map_pos = bitmap_ofs + sizeof(uint32_t) + sizeof(uint8_t);
+	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+	bitmap = read_bitmap_1(bitmap_git);
+	if (!bitmap)
+		return NULL;
+
+	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);
+}
+
+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						      struct commit *commit,
+						      uint32_t *pos_hint)
 {
 	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
 					   commit->object.oid);
-	if (hash_pos >= kh_end(bitmap_git->bitmaps))
+	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
+		uint32_t commit_pos;
+		if (!bitmap_git->table_lookup)
+			return NULL;
+
+		/* NEEDSWORK: cache misses aren't recorded. */
+		if (pos_hint)
+			commit_pos = *pos_hint;
+		else if (!bitmap_table_lookup(bitmap_git,
+					      &commit->object.oid,
+					      &commit_pos))
+			return NULL;
+		return lazy_bitmap_for_commit(bitmap_git, &commit->object.oid,
+					      commit_pos);
+	}
+	return kh_value(bitmap_git->bitmaps, hash_pos);
+}
+
+struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
+				      struct commit *commit)
+{
+	struct stored_bitmap *sb = stored_bitmap_for_commit(bitmap_git, commit,
+							    NULL);
+	if (!sb)
 		return NULL;
-	return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos));
+	return lookup_stored_bitmap(sb);
 }
 
 static inline int bitmap_position_extended(struct bitmap_index *bitmap_git,
@@ -1699,8 +1856,9 @@ void test_bitmap_walk(struct rev_info *revs)
 	if (revs->pending.nr != 1)
 		die("you must specify exactly one commit to test");
 
-	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
-		bitmap_git->version, bitmap_git->entry_count);
+	if (!bitmap_git->table_lookup)
+		fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
+			bitmap_git->version, bitmap_git->entry_count);
 
 	root = revs->pending.objects[0].item;
 	bm = bitmap_for_commit(bitmap_git, (struct commit *)root);
pack-bitmap.h +1 -0
index 3d3ddd77345..37f86787a4d 100644
@@ -26,6 +26,7 @@ struct bitmap_disk_header {
 enum pack_bitmap_opts {
 	BITMAP_OPT_FULL_DAG = 1,
 	BITMAP_OPT_HASH_CACHE = 4,
+	BITMAP_OPT_LOOKUP_TABLE = 16,
 };
 
 enum pack_bitmap_flags {
Signature · 1 line
gitgitgadget
#22 lore raw

PATCH 3/6 pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Teach git to write bitmap lookup table extension. The table has the following information:

- N no of Object ids of each bitmapped commits

- A list of offset, xor-offset pair; the i'th pair denotes the offsets and xor-offsets of i'th commit in the previous list.

- 4-byte integer denoting the flags

---
 pack-bitmap-write.c | 59 +++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 57 insertions(+), 2 deletions(-)
pack-bitmap-write.c +57 -2
index c43375bd344..9e88a64dd65 100644
@@ -650,7 +650,8 @@ static const struct object_id *oid_access(size_t pos, const void *table)
 
 static void write_selected_commits_v1(struct hashfile *f,
 				      struct pack_idx_entry **index,
-				      uint32_t index_nr)
+				      uint32_t index_nr,
+				      off_t *offsets)
 {
 	int i;
 
@@ -663,6 +664,9 @@ static void write_selected_commits_v1(struct hashfile *f,
 		if (commit_pos < 0)
 			BUG("trying to write commit not in index");
 
+		if (offsets)
+			offsets[i] = hashfile_total(f);
+
 		hashwrite_be32(f, commit_pos);
 		hashwrite_u8(f, stored->xor_offset);
 		hashwrite_u8(f, stored->flags);
@@ -671,6 +675,49 @@ static void write_selected_commits_v1(struct hashfile *f,
 	}
 }
 
+static int table_cmp(const void *_va, const void *_vb)
+{
+	return oidcmp(&writer.selected[*(uint32_t*)_va].commit->object.oid,
+		      &writer.selected[*(uint32_t*)_vb].commit->object.oid);
+}
+
+static void write_lookup_table(struct hashfile *f,
+			       off_t *offsets)
+{
+	uint32_t i;
+	uint32_t flags = 0;
+	uint32_t *table, *table_inv;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+	QSORT(table, writer.selected_nr, table_cmp);
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;
+
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		struct object_id *oid = &selected->commit->object.oid;
+
+		hashwrite(f, oid->hash, the_hash_algo->rawsz);
+	}
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+
+		hashwrite_be32(f, offsets[table[i]]);
+		hashwrite_be32(f, selected->xor_offset
+			       ? table_inv[table[i] - selected->xor_offset]
+			       : 0xffffffff);
+	}
+
+	hashwrite_be32(f, flags);
+
+	free(table);
+	free(table_inv);
+}
+
 static void write_hash_cache(struct hashfile *f,
 			     struct pack_idx_entry **index,
 			     uint32_t index_nr)
@@ -695,6 +742,7 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 {
 	static uint16_t default_version = 1;
 	static uint16_t flags = BITMAP_OPT_FULL_DAG;
+	off_t *offsets = NULL;
 	struct strbuf tmp_file = STRBUF_INIT;
 	struct hashfile *f;
 
@@ -715,8 +763,14 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 	dump_bitmap(f, writer.trees);
 	dump_bitmap(f, writer.blobs);
 	dump_bitmap(f, writer.tags);
-	write_selected_commits_v1(f, index, index_nr);
 
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		CALLOC_ARRAY(offsets, index_nr);
+
+	write_selected_commits_v1(f, index, index_nr, offsets);
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		write_lookup_table(f, offsets);
 	if (options & BITMAP_OPT_HASH_CACHE)
 		write_hash_cache(f, index, index_nr);
 
@@ -730,4 +784,5 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 		die_errno("unable to rename temporary bitmap file to '%s'", filename);
 
 	strbuf_release(&tmp_file);
+	free(offsets);
 }
Signature · 1 line
gitgitgadget
#28 lore raw

PATCH 5/6 bitmap-commit-table: add tests for the bitmap lookup table

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add tests to check the working of the newly implemented lookup table.

---
 t/t5310-pack-bitmaps.sh       | 14 ++++++++++++++
 t/t5326-multi-pack-bitmaps.sh | 19 +++++++++++++++++++
 2 files changed, 33 insertions(+)
t/t5310-pack-bitmaps.sh +14 -0
index f775fc1ce69..f05d3e6ace7 100755
@@ -43,6 +43,20 @@ test_expect_success 'full repack creates bitmaps' '
 
 basic_bitmap_tests
 
+test_expect_success 'using lookup table does not affect basic bitmap tests' '
+	test_config pack.writeBitmapLookupTable true &&
+	git repack -adb
+'
+basic_bitmap_tests
+
+test_expect_success 'using lookup table does not let each entries to be parsed one by one' '
+	test_config pack.writeBitmapLookupTable true &&
+	git repack -adb &&
+	git rev-list --test-bitmap HEAD 2>out &&
+	grep "Found bitmap for" out &&
+	! grep "Bitmap v1 test "
+'
+
 test_expect_success 'incremental repack fails when bitmaps are requested' '
 	test_commit more-1 &&
 	test_must_fail git repack -d 2>err &&
t/t5326-multi-pack-bitmaps.sh +19 -0
index 4fe57414c13..85fbdf5e4bb 100755
@@ -306,5 +306,24 @@ test_expect_success 'graceful fallback when missing reverse index' '
 		! grep "ignoring extra bitmap file" err
 	)
 '
+test_expect_success 'multi-pack-index write --bitmap writes lookup table if enabled' '
+	rm -fr repo &&
+	git init repo &&
+	test_when_finished "rm -fr repo" &&
+	(
+		cd repo &&
+		test_commit_bulk 106 &&
+
+		git repack -d &&
+
+		git config pack.writeBitmapLookupTable true &&
+		git multi-pack-index write --bitmap &&
+
+		git rev-list --test-bitmap HEAD 2>out &&
+		grep "Found bitmap for" out &&
+		! grep "Bitmap v1 test "
+
+	)
+'
 
 test_done
Signature · 1 line
gitgitgadget
#26 lore raw

PATCH 4/6 builtin/pack-objects.c: learn pack.writeBitmapLookupTable

In reply to Abhradeep Chakraborty via GitGitGadget

From: Taylor Blau <[email protected]>

Teach git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'.

---
 Documentation/config/pack.txt | 7 +++++++
 builtin/pack-objects.c        | 8 ++++++++
 2 files changed, 15 insertions(+)
Documentation/config/pack.txt +7 -0
index ad7f73a1ead..e12008d2415 100644
@@ -164,6 +164,13 @@ When writing a multi-pack reachability bitmap, no new namehashes are
 computed; instead, any namehashes stored in an existing bitmap are
 permuted into their appropriate location when writing a new bitmap.
 
+pack.writeBitmapLookupTable::
+	When true, git will include a "lookup table" section in the
+	bitmap index (if one is written). This table is used to defer
+	loading individual bitmaps as late as possible. This can be
+	beneficial in repositories which have relatively large bitmap
+	indexes. Defaults to false.
+
 pack.writeReverseIndex::
 	When true, git will write a corresponding .rev file (see:
 	link:../technical/pack-format.html[Documentation/technical/pack-format.txt])
builtin/pack-objects.c +8 -0
index cc5f41086da..3ba20301980 100644
@@ -3148,6 +3148,14 @@ static int git_pack_config(const char *k, const char *v, void *cb)
 		else
 			write_bitmap_options &= ~BITMAP_OPT_HASH_CACHE;
 	}
+
+	if (!strcmp(k, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(k, v))
+			write_bitmap_options |= BITMAP_OPT_LOOKUP_TABLE;
+		else
+			write_bitmap_options &= ~BITMAP_OPT_LOOKUP_TABLE;
+	}
+
 	if (!strcmp(k, "pack.usebitmaps")) {
 		use_bitmap_index_default = git_config_bool(k, v);
 		return 0;
Signature · 1 line
gitgitgadget
#30 lore raw

PATCH 6/6 bitmap-lookup-table: add performance tests

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add performance tests for bitmap lookup table extension.

---
 t/perf/p5310-pack-bitmaps.sh       | 60 +++++++++++++++++++-----------
 t/perf/p5326-multi-pack-bitmaps.sh | 55 +++++++++++++++++----------
 2 files changed, 73 insertions(+), 42 deletions(-)
t/perf/p5310-pack-bitmaps.sh +38 -22
index 7ad4f237bc3..a8d9414de92 100755
@@ -10,10 +10,11 @@ test_perf_large_repo
 # since we want to be able to compare bitmap-aware
 # git versus non-bitmap git
 #
-# We intentionally use the deprecated pack.writebitmaps
+# We intentionally use the deprecated pack.writeBitmaps
 # config so that we can test against older versions of git.
 test_expect_success 'setup bitmap config' '
-	git config pack.writebitmaps true
+	git config pack.writeBitmaps true &&
+	git config pack.writeReverseIndex true
 '
 
 # we need to create the tag up front such that it is covered by the repack and
@@ -28,27 +29,42 @@ test_perf 'repack to disk' '
 
 test_full_bitmap
 
-test_expect_success 'create partial bitmap state' '
-	# pick a commit to represent the repo tip in the past
-	cutoff=$(git rev-list HEAD~100 -1) &&
-	orig_tip=$(git rev-parse HEAD) &&
-
-	# now kill off all of the refs and pretend we had
-	# just the one tip
-	rm -rf .git/logs .git/refs/* .git/packed-refs &&
-	git update-ref HEAD $cutoff &&
-
-	# and then repack, which will leave us with a nice
-	# big bitmap pack of the "old" history, and all of
-	# the new history will be loose, as if it had been pushed
-	# up incrementally and exploded via unpack-objects
-	git repack -Ad &&
-
-	# and now restore our original tip, as if the pushes
-	# had happened
-	git update-ref HEAD $orig_tip
+test_perf 'use lookup table' '
+    git config pack.writeBitmapLookupTable true
 '
 
-test_partial_bitmap
+test_perf 'repack to disk (lookup table)' '
+    git repack -adb
+'
+
+test_full_bitmap
+
+for i in false true
+do
+	$i && lookup=" (lookup table)"
+	test_expect_success "create partial bitmap state$lookup" '
+		git config pack.writeBitmapLookupTable '"$i"' &&
+		# pick a commit to represent the repo tip in the past
+		cutoff=$(git rev-list HEAD~100 -1) &&
+		orig_tip=$(git rev-parse HEAD) &&
+
+		# now kill off all of the refs and pretend we had
+		# just the one tip
+		rm -rf .git/logs .git/refs/* .git/packed-refs &&
+		git update-ref HEAD $cutoff &&
+
+		# and then repack, which will leave us with a nice
+		# big bitmap pack of the "old" history, and all of
+		# the new history will be loose, as if it had been pushed
+		# up incrementally and exploded via unpack-objects
+		git repack -Ad &&
+
+		# and now restore our original tip, as if the pushes
+		# had happened
+		git update-ref HEAD $orig_tip
+	'
+
+	test_partial_bitmap
+done
 
 test_done
t/perf/p5326-multi-pack-bitmaps.sh +35 -20

62 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#2 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 15 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit oids, along with their offset and xor offset. This way git can load only the neccesary bitmaps without loading the previous bitmaps.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

Documentation/technical/bitmap-format.txt +0 -0
@@ -67,6 +67,14 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 77

Quoted 10 lines
Documentation/technical/bitmap-format.txt +8 -0
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0xf) : :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` object ids, a list of pairs of
+			offset and xor offset of respective objects, and 4-byte
+			integer denoting the flags (currently none). The format
+			and meaning of the table is described below.
+

Here, you are adding a new flag that indicates that the end of the file contains this extra extension. This works because the size of the extension is predictable. As long as any future extensions are also of a predictable size, then we can continue adding them via flags in this way.

This is better than updating the full file format to do something like like use the chunk format API, especially because this format is shared across other tools (JGit being mentioned frequently).

It might be worth mentioning in your commit message what happens when an older version of Git (or JGit) notices this flag. Does it refuse to operate on the .bitmap file? Does it give a warning or die? It would be nice if this extension could be ignored (it seems like adding the extra data at the end does not stop the bitmap data from being understood).

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 222

Quoted 7 lines
Documentation/technical/bitmap-format.txt +7 -0
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the end of the `.bitmap`
+contains a lookup table specifying the positions of commits which have a
+bitmap.

Perhaps it would be better to say "the last N * (HASH_LEN + 8) + 4 bytes preceding the trailing hash" or something? This gives us a concrete way to compute the start of the table, while also being clear that the table is included in the trailing hash.

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 227

Quoted 4 lines
Documentation/technical/bitmap-format.txt +4 -0
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the format
+is as follows:
+
+	- `nr_entries` object names.

Could you expand that these objects are commit OIDs, one for each bitmap in the file. Are they sorted in lexicographical order for binary search, or are we expecting to read the entire table into a hashtable in-memory?

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 234

Quoted 6 lines
Documentation/technical/bitmap-format.txt +6 -0
+	- `nr_entries` pairs of 4-byte integers, each in network order.
+	  The first holds the offset from which that commit's bitmap can
+	  be read. The second number holds the position of the commit
+	  whose bitmap the current bitmap is xor'd with in lexicographic
+	  order, or 0xffffffff if the current commit is not xor'd with
+	  anything.

Interesting to give the xor chains directions here. You say "position" here for the second commit: do you mean within the list of object names as opposed to the offset? That would make the most sense so we can trace the full list of XORs we need to make all at once.

Are .bitmap files already constrained to 4GB, so these 32-bit offsets make sense? Using 64-bit offsets would be a small cost here, I think, without needing to do any fancy "overflow" tables that could introduce a variable-length extension.

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 238

Quoted 3 lines
Documentation/technical/bitmap-format.txt +3 -0
+	- One 4-byte network byte order integer specifying
+	  table-specific flags. None exist currently, so this is always
+	  "0".

I'm guessing this is at the end of the extension because a future flag could modify the length of the extension, so we need the flags to be in a predictable location. Could we make that clear somewhere?

How does Git react to seeing flags here that it does not recognize? It seems that Git should ignore the lookup table but continue using the rest of the .bitmap file as it did before, yes?

Thanks,
-Stolee

#3 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

Quoted 18 lines · Derrick Stolee
Quoted 17 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit oids, along with their offset and xor offset. This way git can load only the neccesary bitmaps without loading the previous bitmaps.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

Documentation/technical/bitmap-format.txt +0 -0
@@ -67,6 +67,14 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
Quoted 14 lines

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 77

Quoted 8 lines
Documentation/technical/bitmap-format.txt +8 -0
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0xf) : :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` object ids, a list of pairs of
+			offset and xor offset of respective objects, and 4-byte
+			integer denoting the flags (currently none). The format
+			and meaning of the table is described below.
+

Here, you are adding a new flag that indicates that the end of the file contains this extra extension. This works because the size of the extension is predictable. As long as any future extensions are also of a predictable size, then we can continue adding them via flags in this way.

Right; any extensions that are added to the existing .bitmap format must have a size that is predictable in order for readers to locate the next extension, if any.

Quoted 3 lines

This is better than updating the full file format to do something like like use the chunk format API, especially because this format is shared across other tools (JGit being mentioned frequently).

Agreed. Abhradeep and I discussed whether or not it was worth exploring a new .bitmap format, and the consensus we reached was that it may be required in the future (if we explored a compression scheme other than EWAH or made some other backwards-incompatible change), but as of yet it isn't necessary. So we avoided it to eliminate unnecessary churn, especially of on-disk formats.

Quoted 5 lines

It might be worth mentioning in your commit message what happens when an older version of Git (or JGit) notices this flag. Does it refuse to operate on the .bitmap file? Does it give a warning or die? It would be nice if this extension could be ignored (it seems like adding the extra data at the end does not stop the bitmap data from being understood).

I agree. The bitmap reader does not warn or die when it sees unrecognized extensions, that way new extensions can be added without rendering all previously-written bitmaps useless. But in order to understand an extension on bit N, the reader must also understand extensions N-1, N-2, and so on (in order to locate the end of extension N).

Quoted 12 lines

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 234

Quoted 6 lines
Documentation/technical/bitmap-format.txt +6 -0
+	- `nr_entries` pairs of 4-byte integers, each in network order.
+	  The first holds the offset from which that commit's bitmap can
+	  be read. The second number holds the position of the commit
+	  whose bitmap the current bitmap is xor'd with in lexicographic
+	  order, or 0xffffffff if the current commit is not xor'd with
+	  anything.

Interesting to give the xor chains directions here. You say "position" here for the second commit: do you mean within the list of object names as opposed to the offset? That would make the most sense so we can trace the full list of XORs we need to make all at once.

Quoted 4 lines

Are .bitmap files already constrained to 4GB, so these 32-bit offsets make sense? Using 64-bit offsets would be a small cost here, I think, without needing to do any fancy "overflow" tables that could introduce a variable-length extension.

Yeah, we should support >4GB bitmaps here. An overflow table could work, but I agree with Stolee that in practice it won't matter. Most .bitmap files that I've looked at in the wild have around ~500 entries at most, and are usually small. So the cost of widening this section isn't a big deal.

But note that the entry count is only one component of the bitmap size:
the individual entry lengths obviously matter too. And in repositories whose bitmaps exceed 500 entries, the entries themselves are often several million bits long (before compression) already. So it is certainly possible to exceed 4GB without having an astronomical entry count.

So doubling the width of this extension might add an extra 250 KiB or so, which is negligible.

I would much rather see us do that in cases where it makes sense (small number of entries, minimal cost to wider records, etc.) than adding unnecessary complexity via an extra lookup table for >4GB offsets.

Quoted 7 lines

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 238

Quoted 3 lines
Documentation/technical/bitmap-format.txt +3 -0
+	- One 4-byte network byte order integer specifying
+	  table-specific flags. None exist currently, so this is always
+	  "0".

I'm guessing this is at the end of the extension because a future flag could modify the length of the extension, so we need the flags to be in a predictable location. Could we make that clear somewhere?

I can't remember what I had on my mind when I wrote this ;-).

Abhradeep -- do you have any thoughts about what this might be used for? I'll try to remember it myself, but I imagine that we could just as easily remove this altogether and avoid the confusion.

Quoted 3 lines

How does Git react to seeing flags here that it does not recognize? It seems that Git should ignore the lookup table but continue using the rest of the .bitmap file as it did before, yes?

(See above).

Thanks,
Taylor

#7 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 8 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit oids, along with their offset and xor offset. This way git can load only the neccesary bitmaps without loading the previous bitmaps.

Well put. It might help to have a concrete example of where we expect this to help and not help. I suspect that some of this will show up in your work updating the perf suite to use this new table, but I imagine that we'll find something like:

In cases where the result can be read or computed without significant additional traversal (e.g., all commits of interest already have bitmaps computed), we can save some time loading and parsing a majority of the bitmap file that we will never read.

But in cases where the bitmaps are out-of-date, or there is significant traversal required to go from the reference tips to what's contained in the .bitmap file, this table provides minimal benefit (or something).

Of course, you should verify that that is actually true before we insert it into the commit message as such ;-). But that sort of information may help readers understand what the purpose of this change is towards the beinning of the series.

Quoted 5 lines

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

Here and elsewhere: I typically use my <[email protected]> address when contributing to Git. So any trailers that mention my email or commits that you send on my behalf should use that address, too.

Quoted 11 lines
---
 Documentation/technical/bitmap-format.txt | 31 +++++++++++++++++++++++
 1 file changed, 31 insertions(+)
Documentation/technical/bitmap-format.txt +0 -0
index 04b3ec21785..34e98787b78 100644

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 71

Quoted 5 lines
Documentation/technical/bitmap-format.txt +2 -0
@@ -67,6 +67,14 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0xf) : :::

It the space between "(0xf)" and the first ":" intentional? Similarly, should there be two or three colons at the end (either "::" or ":::")?

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 77

Quoted 6 lines
Documentation/technical/bitmap-format.txt +6 -0
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` object ids, a list of pairs of
+			offset and xor offset of respective objects, and 4-byte
+			integer denoting the flags (currently none). The format
+			and meaning of the table is described below.
+

I remember we had a brief off-list discussion about whether we should store the full object IDs in the offset table, or whether we could store their pack- or index-relative ordering. Is there a reason to prefer one or the other?

I don't think we need to explain the choice fully in the documentation in this patch, but it may be worth thinking about separately nonetheless. We can store either order and convert it to an object ID in constant time.

To figure out which is best, I would recommend trying a few different choices here and seeing how they do or don't impact your performance testing.

Quoted 16 lines
Documentation/technical/bitmap-format.txt +9 -0
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +213,26 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the end of the `.bitmap`
+contains a lookup table specifying the positions of commits which have a
+bitmap.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the format

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 234

Quoted 10 lines
Documentation/technical/bitmap-format.txt +10 -0
+is as follows:
+
+	- `nr_entries` object names.
+
+	- `nr_entries` pairs of 4-byte integers, each in network order.
+	  The first holds the offset from which that commit's bitmap can
+	  be read. The second number holds the position of the commit
+	  whose bitmap the current bitmap is xor'd with in lexicographic
+	  order, or 0xffffffff if the current commit is not xor'd with
+	  anything.

A couple of small thoughts here. I wonder if we'd get better locality if we made each record look something like:

(object_id, offset, xor_pos)

Where object_id is either 20- or 4-bytes long (depending if we store the full object ID, or some 4-byte identifier that allows us to discover it), offset is 8 bytes long, and xor_pos is 4-bytes (since in practice we don't support packs or MIDXs which have more than 2^32-1 objects).

In the event that this table doesn't fit into a single cache line, I think we'll get better performance out of reading it by not forcing the cache to evict itself whenever we need to refer back to the object_id.

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 238

Quoted 3 lines
Documentation/technical/bitmap-format.txt +3 -0
+	- One 4-byte network byte order integer specifying
+	  table-specific flags. None exist currently, so this is always
+	  "0".

I mentioned in my reply to Stolee earlier, but I think that we should either (a) try to remember what this is for and document it, or (b) remove it.

Thanks,
Taylor

#11 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 71

Quoted 4 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Documentation/technical/bitmap-format.txt +2 -0
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0xf) : :::

I think you mean 0x10 (b_1_0000) instead of 0xf (b_1111).

I noticed when looking at the constant in patch 2.

Thanks,
-Stolee

#15 lore raw

Re: [PATCH 2/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 4 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Bitmap lookup table extension can let git to parse only the necessary bitmaps without loading the previous bitmaps one by one.

Here is an attempt to reword this a bit:

The bitmap lookup table extension was documented by an earlier change, but Git does not yet know how to parse that information. The extension allows parsing a smaller portion of the bitmap file in order to find bitmaps for specific commits.

Quoted 1 line

Teach git to read and use the bitmap lookup table extension.

Normally, I don't mind doing the read portion after the write portion, but it would be nice to have them in the opposite order so we can test writing the extension _and Git ignoring the extension_ before implementing the parsing. As it stands, most of the code in this patch is untested until patch 5.

General outline attempt:

1. Document the format.
2. Write the extension if the flag is given.
3. Add pack.writeBitmapLookupTable and add tests that write the lookup table (and do other bitmap reads on that data).
4. Read the lookup table. The tests from step 3 already cover acting upon the lookup table. (Perhaps we add a mode here that disables GIT_READ_COMMIT_TABLE since that is not used anywhere else.)
5. Performance tests.

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 198

Quoted 2 lines
pack-bitmap.c +2 -0
+		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
+		    git_env_bool("GIT_READ_COMMIT_TABLE", 1)) {

This environment variable does not appear to be used or documented anywhere. Do we really want to use it as a way to disable reading the lookup table in general? Or would it be better to have a GIT_TEST_* variable for disabling the read during testing?

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 204

Quoted 6 lines
pack-bitmap.c +6 -0
+			uint32_t entry_count = ntohl(header->entry_count);
+			uint32_t table_size =
+				(entry_count * the_hash_algo->rawsz) /* oids */ +
+				(entry_count * sizeof(uint32_t)) /* offsets */ +
+				(entry_count * sizeof(uint32_t)) /* xor offsets */ +
+				(sizeof(uint32_t)) /* flags */;

Here, uint32_T is probably fine, but maybe we should just use size_t instead? Should we use st_mult() and st_add() everywhere?

Note: you're using the_hash_algo->rawsz here, which makes sense because the bitmap format doesn't specify which hash algorithm is used. Just making this note to say that we should include the hash algorithm as a value in the bitmap format when we increment the format version (in the future).

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 210

Quoted 5 lines
pack-bitmap.c +5 -0
+			if (table_size > index_end - index->map - header_size)
+				return error("corrupted bitmap index file (too short to fit commit table)");
+
+			index->table_lookup = (void *)(index_end - table_size);
+			index->table_offsets = index->table_lookup + the_hash_algo->rawsz * entry_count;

st_mult(), st_add()? Or, should we assume safety now?

pack-bitmap.c static int load_bitmap(struct bitmap_index *bitmap_git) · line 500

Quoted 6 lines
pack-bitmap.c +3 -1
+			index_end -= table_size;
+		}
 
-	if (load_bitmap_entries_v1(bitmap_git) < 0)
+	if (!bitmap_git->table_lookup && load_bitmap_entries_v1(bitmap_git) < 0)
 		goto failed;

Ok, don't load these entries pre-emptively if we have the lookup table.

pack-bitmap.c struct include_data { · line 588

Quoted 3 lines
pack-bitmap.c +3 -0
+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						      struct commit *commit,
+						      uint32_t *pos_hint);

I see that we have a two-method recursion loop. Please move this declaration to immediately before lazy_bitmap_for_commit() so it is declared as late as possible.

pack-bitmap.c struct include_data { · line 594

Quoted 5 lines
pack-bitmap.c +5 -0
+static inline const unsigned char *bitmap_oid_pos(struct bitmap_index *bitmap_git,
+						  uint32_t pos)
+{
+	return bitmap_git->table_lookup + (pos * the_hash_algo->rawsz);
+}

I would call this "bitmap_hash_pos()" because we are getting a raw hash and not a 'struct object_id'. Do you want a helper that fills a 'struct object_id', perhaps passed-by-reference?

pack-bitmap.c struct include_data { · line 603

Quoted 5 lines
pack-bitmap.c +4 -0
+static inline const void *bitmap_offset_pos(struct bitmap_index *bitmap_git,
+					    uint32_t pos)
 
+static inline const void *xor_position_pos(struct bitmap_index *bitmap_git,
+					   uint32_t pos)

These two helpers should probably return a size_t and uint32_t instead of a pointer. Let these do get_be[32|64]() on the computed pointer.

pack-bitmap.c struct include_data { · line 622

Quoted 10 lines
pack-bitmap.c +10 -0
+static int bitmap_table_lookup(struct bitmap_index *bitmap_git,
+			       struct object_id *oid,
+			       uint32_t *commit_pos)
+{
+	unsigned char *found = bsearch(oid->hash, bitmap_git->table_lookup,
+				       bitmap_git->entry_count,
+				       the_hash_algo->rawsz, bitmap_lookup_cmp);
+	if (found)
+		*commit_pos = (found - bitmap_git->table_lookup) / the_hash_algo->rawsz;
+	return !!found;

Ok, we are running binary search and converting the pointer into a position.

Frequently, these kind of searches return an int, but use a negative value to indicate that the value was not found. Using an int in this way would restrict us to 2^31 bitmaps instead of 2^32, so maybe it is not worth matching that practice.

pack-bitmap.c struct include_data { · line 637

Quoted 13 lines
pack-bitmap.c +13 -0
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						    struct object_id *oid,
+						    uint32_t commit_pos)
+{
+	uint32_t xor_pos;
+	off_t bitmap_ofs;
+
+	int flags;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap;
+
+	bitmap_ofs = get_be32(bitmap_offset_pos(bitmap_git, commit_pos));
+	xor_pos = get_be32(xor_position_pos(bitmap_git, commit_pos));

These lines become simpler with a change in the helper methods' prototypes, as I recommended higher up.

Quoted 11 lines
pack-bitmap.c +11 -0
+	/*
+	 * Lazily load the xor'd bitmap if required (and we haven't done so
+	 * already). Make sure to pass the xor'd bitmap's position along as a
+	 * hint to avoid an unnecessary binary search in
+	 * stored_bitmap_for_commit().
+	 */
+	if (xor_pos == 0xffffffff) {
+		xor_bitmap = NULL;
+	} else {
+		struct commit *xor_commit;
+		struct object_id xor_oid;

pack-bitmap.c struct include_data { · line 659

Quoted 10 lines
pack-bitmap.c +10 -0
+
+		oidread(&xor_oid, bitmap_oid_pos(bitmap_git, xor_pos));
+
+		xor_commit = lookup_commit(the_repository, &xor_oid);
+		if (!xor_commit)
+			return NULL;
+
+		xor_bitmap = stored_bitmap_for_commit(bitmap_git, xor_commit,
+						      &xor_pos);
+	}

This is using an interesting type of tail-recursion. We might be better off using a loop with a stack: push to the stack the commit positions of the XOR bitmaps. At the very bottom, we get a bitmap without an XOR base. Then, pop off the stack, modifying the bitmap with XOR operations as we go. (Perhaps we also store these bitmaps in-memory along the way?) Finally, we have the necessary bitmap.

This iterative approach avoids possible stack exhaustion if there are long XOR chains in the file.

Quoted 21 lines
pack-bitmap.c +21 -0
+
+	/*
+	 * Don't bother reading the commit's index position or its xor
+	 * offset:
+	 *
+	 *   - The commit's index position is irrelevant to us, since
+	 *     load_bitmap_entries_v1 only uses it to learn the object
+	 *     id which is used to compute the hashmap's key. We already
+	 *     have an object id, so no need to look it up again.
+	 *
+	 *   - The xor_offset is unusable for us, since it specifies how
+	 *     many entries previous to ours we should look at. This
+	 *     makes sense when reading the bitmaps sequentially (as in
+	 *     load_bitmap_entries_v1()), since we can keep track of
+	 *     each bitmap as we read them.
+	 *
+	 *     But it can't work for us, since the bitmap's don't have a
+	 *     fixed size. So we learn the position of the xor'd bitmap
+	 *     from the commit table (and resolve it to a bitmap in the
+	 *     above if-statement).
+	 *

pack-bitmap.c struct include_data { · line 690

Quoted 10 lines
pack-bitmap.c +10 -0
+	 * Instead, we can skip ahead and immediately read the flags and
+	 * ewah bitmap.
+	 */
+	bitmap_git->map_pos = bitmap_ofs + sizeof(uint32_t) + sizeof(uint8_t);
+	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+	bitmap = read_bitmap_1(bitmap_git);
+	if (!bitmap)
+		return NULL;
+
+	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);

Looks like we'd want to call store_bitmap() while popping the stack in the loop I recommended above.

Quoted 13 lines
pack-bitmap.c +9 -1
+}
+
+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						      struct commit *commit,
+						      uint32_t *pos_hint)
 {
 	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
 					   commit->object.oid);
-	if (hash_pos >= kh_end(bitmap_git->bitmaps))
+	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
+		uint32_t commit_pos;
+		if (!bitmap_git->table_lookup)
+			return NULL;

pack-bitmap.c struct include_data { · line 712

Quoted 10 lines
pack-bitmap.c +10 -0
+
+		/* NEEDSWORK: cache misses aren't recorded. */
+		if (pos_hint)
+			commit_pos = *pos_hint;
+		else if (!bitmap_table_lookup(bitmap_git,
+					      &commit->object.oid,
+					      &commit_pos))
+			return NULL;
+		return lazy_bitmap_for_commit(bitmap_git, &commit->object.oid,
+					      commit_pos);

The extra bonus of going incremental is that we don't have recursion across two methods, which I always find difficult to reason about.

pack-bitmap.c struct bitmap_disk_header { · line 29

Quoted 9 lines
pack-bitmap.c +4 -0
+	}
+	return kh_value(bitmap_git->bitmaps, hash_pos);
+}
@@ -26,6 +26,7 @@ struct bitmap_disk_header {
 enum pack_bitmap_opts {
 	BITMAP_OPT_FULL_DAG = 1,
 	BITMAP_OPT_HASH_CACHE = 4,
+	BITMAP_OPT_LOOKUP_TABLE = 16,

Perhaps it is time to use hexadecimal representation here to match the file format document?

Thanks,
-Stolee

#17 lore raw

Re: [PATCH 2/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 29 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Bitmap lookup table extension can let git to parse only the necessary bitmaps without loading the previous bitmaps one by one.

Teach git to read and use the bitmap lookup table extension.

---
 pack-bitmap.c | 172 ++++++++++++++++++++++++++++++++++++++++++++++++--
 pack-bitmap.h |   1 +
 2 files changed, 166 insertions(+), 7 deletions(-)
pack-bitmap.c +1 -0
index 36134222d7a..d5e5973a79f 100644
@@ -15,6 +15,7 @@
 #include "list-objects-filter-options.h"
 #include "midx.h"
 #include "config.h"
+#include "hash-lookup.h"
 
 /*
  * An entry on the bitmap index, representing the bitmap for a given

pack-bitmap.c struct bitmap_index { · line 92

Quoted 10 lines
pack-bitmap.c +7 -0
@@ -82,6 +83,13 @@ struct bitmap_index {
 	/* The checksum of the packfile or MIDX; points into map. */
 	const unsigned char *checksum;
 
+	/*
+	 * If not NULL, these point into the various commit table sections
+	 * (within map).
+	 */
+	unsigned char *table_lookup;
+	unsigned char *table_offsets;
+

If table_offsets ends up being a list of just offsets, we could assign this to the appropriate type, e.g., 'uint64_t *'. We would want to avoid using a type whose width is platform dependent, like off_t.

But if you end up taking my suggestion from a previous response (of making each entry in the offset table a triple of commit, offset, and xor position), make sure to _not_ get tempted to define a struct and assign table_lookup to be a pointer of that structure type.

That's because even though the struct *should* be packed as you expect, the packing is mostly up to the compiler, so you can't guarantee its members won't have padding between them or at the end of the struct for alignment purposes.

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 198

Quoted 10 lines
pack-bitmap.c +3 -0
 	/*
 	 * Extended index.
 	 *
@@ -185,6 +193,24 @@ static int load_bitmap_header(struct bitmap_index *index)
 			index->hashes = (void *)(index_end - cache_size);
 			index_end -= cache_size;
 		}
+
+		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
+		    git_env_bool("GIT_READ_COMMIT_TABLE", 1)) {

What is the purpose of the GIT_READ_COMMIT_TABLE environment variable? I assume that it's to make it easier to run tests (especially performance ones) with and without access to the lookup table. If so, we should document that (lightly) in the commit message, and rename this to be GIT_TEST_READ_COMMIT_TABLE to indicate that it shouldn't be used outside of tests.

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 204

Quoted 6 lines
pack-bitmap.c +6 -0
+			uint32_t entry_count = ntohl(header->entry_count);
+			uint32_t table_size =
+				(entry_count * the_hash_algo->rawsz) /* oids */ +
+				(entry_count * sizeof(uint32_t)) /* offsets */ +
+				(entry_count * sizeof(uint32_t)) /* xor offsets */ +
+				(sizeof(uint32_t)) /* flags */;

entry_count is definitely a 4-byte integer, so uint32_t is the right type. But I think table_size should be a size_t, and computations on it should be more strictly checked. Perhaps something like;

    size_t table_size = sizeof(uint32_t); /* flags */
    table_size = st_add(table_size, st_mult(entry_count, the_hash_algo->rawsz)); /* oids */
    table_size = st_add(table_size, st_mult(entry_count, sizeof(uint32_t))); /* offsets */
    table_size = st_add(table_size, st_mult(entry_count, sizeof(uint32_t))); /* xor offsets */

or even:

    size_t table_size = sizeof(uint32_t); /* flags */
    table_size = st_add(table_size,
                        st_mult(entry_count,
                                the_hash_algo->rawsz + /* oids */
                                sizeof(uint32_t) + /* offsets*/
                                sizeof(uint32_t) /* xor offsets */
                               ));

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 213

Quoted 8 lines
pack-bitmap.c +8 -0
+			if (table_size > index_end - index->map - header_size)
+				return error("corrupted bitmap index file (too short to fit commit table)");
+
+			index->table_lookup = (void *)(index_end - table_size);
+			index->table_offsets = index->table_lookup + the_hash_algo->rawsz * entry_count;
+
+			index_end -= table_size;
+		}

Looks good.

pack-bitmap.c static int load_bitmap(struct bitmap_index *bitmap_git) · line 500

Quoted 7 lines
pack-bitmap.c +1 -1
@@ -470,7 +496,7 @@ static int load_bitmap(struct bitmap_index *bitmap_git)
 		!(bitmap_git->tags = read_bitmap_1(bitmap_git)))
 		goto failed;
 
-	if (load_bitmap_entries_v1(bitmap_git) < 0)
+	if (!bitmap_git->table_lookup && load_bitmap_entries_v1(bitmap_git) < 0)
 		goto failed;

No need to load each of the bitmaps individually via load_bitmap_entries_v1() if we have a lookup table. That function doesn't do any other initialization that we depend on, so it's OK to just avoid calling it altogether.

Quoted 23 lines
pack-bitmap.c +16 -2
 	return 0;
@@ -557,14 +583,145 @@ struct include_data {
 	struct bitmap *seen;
 };
 
-struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
-				      struct commit *commit)
+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						      struct commit *commit,
+						      uint32_t *pos_hint);
+
+static inline const unsigned char *bitmap_oid_pos(struct bitmap_index *bitmap_git,
+						  uint32_t pos)
+{
+	return bitmap_git->table_lookup + (pos * the_hash_algo->rawsz);
+}
+
+static inline const void *bitmap_offset_pos(struct bitmap_index *bitmap_git,
+					    uint32_t pos)
+{
+	return bitmap_git->table_offsets + (pos * 2 * sizeof(uint32_t));
+}
+

pack-bitmap.c struct include_data { · line 611

Quoted 10 lines
pack-bitmap.c +10 -0
+static inline const void *xor_position_pos(struct bitmap_index *bitmap_git,
+					   uint32_t pos)
+{
+	return (unsigned char*) bitmap_offset_pos(bitmap_git, pos) + sizeof(uint32_t);
+}
+
+static int bitmap_lookup_cmp(const void *_va, const void *_vb)
+{
+	return hashcmp(_va, _vb);
+}

All makes sense. Some light documentation might help explain what this comparator function is used for (the bsearch() call below in bitmap_table_lookup()), although I suspect that this function will get slightly more complicated if you pack the table contents as I suggest, in which case more documentation will definitely help.

pack-bitmap.c struct include_data { · line 621

Quoted 10 lines
pack-bitmap.c +10 -0
+
+static int bitmap_table_lookup(struct bitmap_index *bitmap_git,
+			       struct object_id *oid,
+			       uint32_t *commit_pos)
+{
+	unsigned char *found = bsearch(oid->hash, bitmap_git->table_lookup,
+				       bitmap_git->entry_count,
+				       the_hash_algo->rawsz, bitmap_lookup_cmp);
+	if (found)
+		*commit_pos = (found - bitmap_git->table_lookup) / the_hash_algo->rawsz;

If you end up chaning the type of bitmap_git->table_lookup, make sure that you scale the result of the pointer arithmetic accordingly, or cast down to an 'unsigned char *' before you do any math.

Quoted 20 lines
pack-bitmap.c +20 -0
+	return !!found;
+}
+
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						    struct object_id *oid,
+						    uint32_t commit_pos)
+{
+	uint32_t xor_pos;
+	off_t bitmap_ofs;
+
+	int flags;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap;
+
+	bitmap_ofs = get_be32(bitmap_offset_pos(bitmap_git, commit_pos));
+	xor_pos = get_be32(xor_position_pos(bitmap_git, commit_pos));
+
+	/*
+	 * Lazily load the xor'd bitmap if required (and we haven't done so
+	 * already). Make sure to pass the xor'd bitmap's position along as a

pack-bitmap.c struct include_data { · line 651

Quoted 10 lines
pack-bitmap.c +10 -0
+	 * hint to avoid an unnecessary binary search in
+	 * stored_bitmap_for_commit().
+	 */
+	if (xor_pos == 0xffffffff) {
+		xor_bitmap = NULL;
+	} else {
+		struct commit *xor_commit;
+		struct object_id xor_oid;
+
+		oidread(&xor_oid, bitmap_oid_pos(bitmap_git, xor_pos));

Interesting; this is a point that I forgot about from the original patch. xor_pos is an index (not an offset) into the list of commits in the table of contents in the order appear in that table. We should be clear about (a) what that order is, and (b) that xor_pos is an index into that order.

The rest of this function looks good to me.

pack-bitmap.c struct include_data { · line 702

Quoted 11 lines
pack-bitmap.c +7 -1
+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
+						      struct commit *commit,
+						      uint32_t *pos_hint)
 {
 	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
 					   commit->object.oid);
-	if (hash_pos >= kh_end(bitmap_git->bitmaps))
+	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
+		uint32_t commit_pos;
+		if (!bitmap_git->table_lookup)
+			return NULL;

I was going to suggest moving this check into the caller bitmap_for_commit() and making it a BUG() to call stored_bitmap_for_commit() with a NULL bitmap_git->table_lookup pointer.

And I think this makes sense... if we return NULL here, then we know that we definitely don't have a stored bitmap, since there's no table to look it up in and we have already loaded everything else. So we propagate that NULL to the return value of bitmap_for_commit(), and that makes sense. Good.

pack-bitmap.c struct include_data { · line 704

Quoted 1 line
pack-bitmap.c +1 -0
+		/* NEEDSWORK: cache misses aren't recorded. */

Yeah. The problem here is that we can't record every commit that _doesn't_ have a bitmap every time we return NULL from one of these queries, since there are arbitrarily many such commits that don't have bitmaps.

We could approximate it using a Bloom filter or something, and much of that code is already written and could be interesting to try and reuse.

But I wonder if we could get by with something simpler, though, which would cause us to load all bitmaps from the lookup table after a fixed number of cache misses (at which point we should force ourselves to load everything and just read everything out of a single O(1) lookup in the stored bitmap table).

That may or may not be a good idea, and the threshold will probably be highly dependent on the system. So it may not even be worth it, but I think it's an interesting area to experiemnt in and think a little more about.

pack-bitmap.c struct include_data { · line 706

Quoted 2 lines
pack-bitmap.c +2 -0
+		if (pos_hint)
+			commit_pos = *pos_hint;

How does this commit_pos work again? I confess I have forgetten since I wrote some of this code a while ago... :-).

pack-bitmap.c void test_bitmap_walk(struct rev_info *revs) · line 1861

Quoted 9 lines
pack-bitmap.c +3 -2
@@ -1699,8 +1856,9 @@ void test_bitmap_walk(struct rev_info *revs)
 	if (revs->pending.nr != 1)
 		die("you must specify exactly one commit to test");
 
-	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
-		bitmap_git->version, bitmap_git->entry_count);
+	if (!bitmap_git->table_lookup)
+		fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
+			bitmap_git->version, bitmap_git->entry_count);

Should we print this regardless of whether or not there is a lookup table? We should be able to learn the entry count either way.

Thanks,
Taylor

#23 lore raw

Re: [PATCH 3/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 6 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Teach git to write bitmap lookup table extension. The table has the following information:

- N no of Object ids of each bitmapped commits

s/no/number, s/Object/object, s/ids/IDs, and s/commits/commit

Quoted 2 lines

- A list of offset, xor-offset pair; the i'th pair denotes the offsets and xor-offsets of i'th commit in the previous list.

s/pair/pairs

Quoted 25 lines

- 4-byte integer denoting the flags

---
 pack-bitmap-write.c | 59 +++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 57 insertions(+), 2 deletions(-)
pack-bitmap-write.c +2 -1
index c43375bd344..9e88a64dd65 100644
@@ -650,7 +650,8 @@ static const struct object_id *oid_access(size_t pos, const void *table)
 
 static void write_selected_commits_v1(struct hashfile *f,
 				      struct pack_idx_entry **index,
-				      uint32_t index_nr)
+				      uint32_t index_nr,
+				      off_t *offsets)
 {
 	int i;
 

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 669

Quoted 6 lines
pack-bitmap-write.c +3 -0
@@ -663,6 +664,9 @@ static void write_selected_commits_v1(struct hashfile *f,
 		if (commit_pos < 0)
 			BUG("trying to write commit not in index");
 
+		if (offsets)
+			offsets[i] = hashfile_total(f);
+

Makes sense; we record the offset for the ith commit as however many bytes we've already written into the hashfile up to this point, since the subsequent byte will begin the bitmap (well, the preceding few bytes of it, anyways) itself.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 681

Quoted 11 lines
pack-bitmap-write.c +4 -0
 		hashwrite_be32(f, commit_pos);
 		hashwrite_u8(f, stored->xor_offset);
 		hashwrite_u8(f, stored->flags);
@@ -671,6 +675,49 @@ static void write_selected_commits_v1(struct hashfile *f,
 	}
 }
 
+static int table_cmp(const void *_va, const void *_vb)
+{
+	return oidcmp(&writer.selected[*(uint32_t*)_va].commit->object.oid,
+		      &writer.selected[*(uint32_t*)_vb].commit->object.oid);

This implementation looks right to me, but perhaps we should expand it out from the one-liner here to make it more readable. Perhaps something like:

    static int table_cmp(const void *_va, const void *_vb)
    {
      struct commit *c1 = &writer.selected[*(uint32_t*)_va];
      struct commit *c2 = &writer.selected[*(uint32_t*)_vb];

      return oidcmp(&c1->object.oid, &c2->object.oid);
    }

which is arguably slightly more readable than the one-liner (but I don't feel that strongly about it.)

Quoted 5 lines
pack-bitmap-write.c +5 -0
+static void write_lookup_table(struct hashfile *f,
+			       off_t *offsets)
+{
+	uint32_t i;
+	uint32_t flags = 0;

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 698

Quoted 10 lines
pack-bitmap-write.c +10 -0
+	uint32_t *table, *table_inv;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+	QSORT(table, writer.selected_nr, table_cmp);
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;

Right... so table[0] will give us the index into writer.selected of the commit with the earliest OID in lexicographic order. And table_inv goes the other way around: table_inv[i] will tell us the lexicographic position of the commit at writer.selected[i].

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 711

Quoted 12 lines
pack-bitmap-write.c +12 -0
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		struct object_id *oid = &selected->commit->object.oid;
+
+		hashwrite(f, oid->hash, the_hash_algo->rawsz);
+	}
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+
+		hashwrite_be32(f, offsets[table[i]]);
+		hashwrite_be32(f, selected->xor_offset
+			       ? table_inv[table[i] - selected->xor_offset]

...which we need to discover the position of the XOR'd bitmap. Though I'm not sure if I remember why table[i] - selected->xor_offset is right and not i - selected->xor_offset.

Thanks,
Taylor

#27 lore raw

Re: [PATCH 4/6] builtin/pack-objects.c: learn pack.writeBitmapLookupTable

In reply to Taylor Blau via GitGitGadget
Quoted 17 lines · Taylor Blau via GitGitGadget

From: Taylor Blau <[email protected]>

Teach git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'.

---
 Documentation/config/pack.txt | 7 +++++++
 builtin/pack-objects.c        | 8 ++++++++
 2 files changed, 15 insertions(+)
Documentation/config/pack.txt +0 -0
index ad7f73a1ead..e12008d2415 100644

Documentation/config/pack.txt When writing a multi-pack reachability bitmap, no new namehashes are · line 168

Quoted 5 lines
Documentation/config/pack.txt +2 -0
@@ -164,6 +164,13 @@ When writing a multi-pack reachability bitmap, no new namehashes are
 computed; instead, any namehashes stored in an existing bitmap are
 permuted into their appropriate location when writing a new bitmap.
 
+pack.writeBitmapLookupTable::
+	When true, git will include a "lookup table" section in the

s/git/Git (I typically use "git" when talking about the command-line tool, and Git when talking about the project as a proper noun).

Documentation/config/pack.txt When writing a multi-pack reachability bitmap, no new namehashes are · line 172

Quoted 4 lines
Documentation/config/pack.txt +4 -0
+	bitmap index (if one is written). This table is used to defer
+	loading individual bitmaps as late as possible. This can be
+	beneficial in repositories which have relatively large bitmap
+	indexes. Defaults to false.

Is there a reason that we would want to default to "false" here? Perhaps in the first version of two we would want this to be an opt-in (since there is no publicly documented way to opt-out of reading the extension once it is written).

We should make sure to enable this by default at some point in the future.

Documentation/config/pack.txt When writing a multi-pack reachability bitmap, no new namehashes are · line 174

Quoted 1 line
Documentation/config/pack.txt +0 -0
 pack.writeReverseIndex::

...since it's easy to forget ;-).

Thanks,
Taylor

#6 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Derrick Stolee
Quoted 5 lines · Derrick Stole

It might be worth mentioning in your commit message what happens when an older version of Git (or JGit) notices this flag. Does it refuse to operate on the .bitmap file? Does it give a warning or die? It would be nice if this extension could be ignored (it seems like adding the extra data at the end does not stop the bitmap data from being understood).

No, it doesn't refuse to operate on the .bitmap file. It just ignores the extension. Will update the commit message.

Quoted 4 lines

Perhaps it would be better to say "the last N * (HASH_LEN + 8) + 4 bytes preceding the trailing hash" or something? This gives us a concrete way to compute the start of the table, while also being clear that the table is included in the trailing hash.

Hmm, well said. Will update it.

Quoted 3 lines

Could you expand that these objects are commit OIDs, one for each bitmap in the file. Are they sorted in lexicographical order for binary search, or are we expecting to read the entire table into a hashtable in-memory?

Yeah, of course! They are sorted in lexicographical order for binary search.

Quoted 4 lines

Interesting to give the xor chains directions here. You say "position" here for the second commit: do you mean within the list of object names as opposed to the offset? That would make the most sense so we can trace the full list of XORs we need to make all at once.

I think I blundered here. I forgot that the xor-offset is relative to the current bitmap. The current proposed code takes it as ABSOLUTE value and tries to find the commit on that position (in the list of commit ids). So, there are two faults in my code - (1) As the xor-offset have an upper limit (which is 10 probably; not sure), any of the first 10 commits is always selected. (2) As xor-offsets are relative to the current bitmap, it depends On the order of the bitmaps. These bitmaps are ordered by the date of their corresponding commit and commit ids in the lookup table are ordered lexicographically. So, we can't use that xor-offset to find the xor'd commit position.

Will fix it.

Quoted 4 lines

Are .bitmap files already constrained to 4GB, so these 32-bit offsets make sense? Using 64-bit offsets would be a small cost here, I think, without needing to do any fancy "overflow" tables that could introduce a variable-length extension.

I think you're right. I should use 64-bit types here.

Quoted 3 lines

I'm guessing this is at the end of the extension because a future flag could modify the length of the extension, so we need the flags to be in a predictable location. Could we make that clear somewhere?

Flags are at the end of this extension.

Thanks :)

#4 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Taylor Blau
Quoted 3 lines · Taylor Blau

Abhradeep -- do you have any thoughts about what this might be used for? I'll try to remember it myself, but I imagine that we could just as easily remove this altogether and avoid the confusion.

Honestly, I never understood the logic behind adding this flag option. I thought you have a reason to do that. Even I was thinking of curving it to 1 byte. I will remove it then.

Thanks :)

#8 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Taylor Blau
Quoted 10 lines · Taylor Blau

In cases where the result can be read or computed without significant additional traversal (e.g., all commits of interest already have bitmaps computed), we can save some time loading and parsing a majority of the bitmap file that we will never read.

But in cases where the bitmaps are out-of-date, or there is significant traversal required to go from the reference tips to what's contained in the .bitmap file, this table provides minimal benefit (or something).

Quoted 4 lines

Of course, you should verify that that is actually true before we insert it into the commit message as such ;-). But that sort of information may help readers understand what the purpose of this change is towards the beinning of the series.

The performance tests cover tests for command like "git rev-list --count --objects --all", "simulated clone", "simulated fetch" etc. And I tested it with both the Git and Linux. In both cases, the average cost of "Without lookup table" is bigger than "with lookup table". The margin of difference is bigger for linux. Though, I need to fix the calculation of xor-offset (see my reply to derrick), the fix will not affect the performance too much. So, what you're saying is true. I think I didn't write the bitmap out-of-date test though.

Quoted 3 lines

Here and elsewhere: I typically use my <[email protected]> address when contributing to Git. So any trailers that mention my email or commits that you send on my behalf should use that address, too.

Ohh, sorry! Will fix it.

Quoted 2 lines

It the space between "(0xf)" and the first ":" intentional? Similarly, should there be two or three colons at the end (either "::" or ":::")?

Yes, it is intentional. My previous patch (formatting the bitmap-format.txt) uses nested description lists. ":::" means it is the level 3 description list. The space is required else asciidoc will assume that it is level 4 description list.

Quoted 5 lines

I remember we had a brief off-list discussion about whether we should store the full object IDs in the offset table, or whether we could store their pack- or index-relative ordering. Is there a reason to prefer one or the other?

Quoted 8 lines

I don't think we need to explain the choice fully in the documentation in this patch, but it may be worth thinking about separately nonetheless. We can store either order and convert it to an object ID in constant time.

To figure out which is best, I would recommend trying a few different choices here and seeing how they do or don't impact your performance testing.

I think at that time I thought it would add extra cost of computing the actual commit ids from those index position. So, I didn't go further here.

I still have a feeling that there is some way to get rid of this list of commit ids. But at the same time, I do not want to add extra computation to the code.

Quoted 5 lines

A couple of small thoughts here. I wonder if we'd get better locality if we made each record look something like:

(object_id, offset, xor_pos)

Quoted 8 lines

Where object_id is either 20- or 4-bytes long (depending if we store the full object ID, or some 4-byte identifier that allows us to discover it), offset is 8 bytes long, and xor_pos is 4-bytes (since in practice we don't support packs or MIDXs which have more than 2^32-1 objects).

In the event that this table doesn't fit into a single cache line, I think we'll get better performance out of reading it by not forcing the cache to evict itself whenever we need to refer back to the object_id.

Ok, will look into it.

Quoted 3 lines

I mentioned in my reply to Stolee earlier, but I think that we should either (a) try to remember what this is for and document it, or (b) remove it.

Let us for now remove it.

Thanks :)

#12 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Derrick Stolee
Quoted 3 lines · Derrick Stolee

I think you mean 0x10 (b_1_0000) instead of 0xf (b_1111).

I noticed when looking at the constant in patch 2.

Yes, you're right. It's kind of embarrassment for me :)

If the flag was Oxf it would enable all the extensions.

#16 lore raw

Re: [PATCH 2/6] pack-bitmap: prepare to read lookup table extension

In reply to Derrick Stolee
Quoted 6 lines · Derrick Stolee

Here is an attempt to reword this a bit:

The bitmap lookup table extension was documented by an earlier change, but Git does not yet know how to parse that information. The extension allows parsing a smaller portion of the bitmap file in order to find bitmaps for specific commits.

Got it. Thanks.

Quoted 5 lines

This environment variable does not appear to be used or documented anywhere. Do we really want to use it as a way to disable reading the lookup table in general? Or would it be better to have a GIT_TEST_* variable for disabling the read during testing?

GIT_TEST_* is perfect. This was mainly for testing purpose.

Quoted 1 line

Here, uint32_T is probably fine, but maybe we should just use

size_t instead? Should we use st_mult() and st_add() everywhere?

Yeah, it would be better to use st_*().

Quoted 3 lines

I see that we have a two-method recursion loop. Please move this declaration to immediately before lazy_bitmap_for_commit() so it is declared as late as possible.

Ok.

Quoted 3 lines

These two helpers should probably return a size_t and uint32_t instead of a pointer. Let these do get_be[32|64]() on the computed pointer.

Ok.

Quoted 6 lines

This is using an interesting type of tail-recursion. We might be better off using a loop with a stack: push to the stack the commit positions of the XOR bitmaps. At the very bottom, we get a bitmap without an XOR base. Then, pop off the stack, modifying the bitmap with XOR operations as we go. (Perhaps we also store these bitmaps in-memory along the way?) Finally, we have the necessary bitmap.

Hmm, got the point. I need to fix the xor-offset related issue first (That I said earlier) before doing this.

Quoted 2 lines

Perhaps it is time to use hexadecimal representation here to match the file format document?

Yeah, of course!

Thanks :)

#18 lore raw

Re: [PATCH 2/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau
Quoted 6 lines · Taylor Blau

What is the purpose of the GIT_READ_COMMIT_TABLE environment variable? I assume that it's to make it easier to run tests (especially performance ones) with and without access to the lookup table. If so, we should document that (lightly) in the commit message, and rename this to be GIT_TEST_READ_COMMIT_TABLE to indicate that it shouldn't be used outside of tests.

This is mainly for testing, GIT_TEST_READ_COMMIT_TABLE is perfect.

Quoted 5 lines

All makes sense. Some light documentation might help explain what this comparator function is used for (the bsearch() call below in bitmap_table_lookup()), although I suspect that this function will get slightly more complicated if you pack the table contents as I suggest, in which case more documentation will definitely help.

Ok.

Quoted 5 lines

Interesting; this is a point that I forgot about from the original patch. xor_pos is an index (not an offset) into the list of commits in the table of contents in the order appear in that table. We should be clear about (a) what that order is, and (b) that xor_pos is an index into that order.

This is exactly what I said in my first reply. I made a mistake here.
(1) As xor_pos is relative to the current bitmap, it depends on the bitmap entry order. These two order are not same. One is ordered by date, another is lexicographically ordered. I will fix it.

Quoted 13 lines

Yeah. The problem here is that we can't record every commit that _doesn't_ have a bitmap every time we return NULL from one of these queries, since there are arbitrarily many such commits that don't have bitmaps.

We could approximate it using a Bloom filter or something, and much of that code is already written and could be interesting to try and reuse. But I wonder if we could get by with something simpler, though, which would cause us to load all bitmaps from the lookup table after a fixed number of cache misses (at which point we should force ourselves to load everything and just read everything out of a single O(1) lookup in the stored bitmap table).

Quoted 4 lines

That may or may not be a good idea, and the threshold will probably be highly dependent on the system. So it may not even be worth it, but I think it's an interesting area to experiemnt in and think a little more about.

Now I got the point. I wonder what if we leave it as it is. How much will it affect the code?

Quoted 2 lines

How does this commit_pos work again? I confess I have forgetten since I wrote some of this code a while ago... :-).

It is using recursive strategy. The first call to stored_bitmap_for_commit function do not have pos_hint. So, it uses bitmap_table_lookup to find the commit position in the list and makes a call to lazy_bitmap_for_commit function. This function gets the offset and xor-offset using the commit id's position in the list. If xor-offset exists, it is using this xor-offset to get the xor-bitmap by calling stored_bitmap_for_commit again. But this time pos_hint is xor-offset. This goes on till the last non-xor bitmap has found.

As I said before, xor-offset should be an absolute value to make it work correctly.

Quoted 2 lines

Should we print this regardless of whether or not there is a lookup table? We should be able to learn the entry count either way.

No, this is necessary. "Bitmap v1 test (%d entries loaded)" means all the bitmap entries has been loaded. It is basically for load_bitmap_entries_bitmap_v1 function which loads all the bitmaps One by one. But if there is a lookup table, prepare_bitmap_git function will not load every entries and thus printing the above line is wrong.

Thanks :)

#24 lore raw

Re: [PATCH 3/6] pack-bitmap-write.c: write lookup table extension

In reply to Taylor Blau
Quoted 2 lines · Taylor Blau

I'm not sure if I remember why table[i] - selected->xor_offset is right and not i - selected->xor_offset.

Even I myself got confused! Before sending the patch to the mailing list, I was clear about that. That's why I didn't catch the so called mistake I have been notifying till now. Thanks Taylor for asking the question!

I should add a comment before the line so that people can understand it. Let us parse table_inv[table[i] - selected->xor_offset] -

Suppose bitmap entries be like -

Bitmap 0 (for commit 0)
Bitmap 1 (for commit 1)
Bitmap 2 (for commit 2)
Bitmap 3 (for commit 3)
.
.
.
Bitmap 20 (for commit 20)

These bitmaps are ordered by the date of their corresponding commit. table array maps commit's lexicographic order to its bitmap order. table_inv stores the reverse (i.e. it maps bitmap order to lexicographic order). Say for example, if commit 4 is lexicographically first among all the Commits then table[0] is 4. Similarly table[1]=2, table[2]=1 etc. table_inv[4] is 0, table_inv[2]=1 etc.

Now suppose commit 4's bitmap has xor-relation with commit 2's bitmap. So, xor-offset for bitmap 4 is 2. And table[0] - selected->xor_offset is equal to 4-2 = 2. It is pointing to the commit 2. Now, 2 is in bitmap Order. We need to convert it into lexicographic order. So, table_inv[2] gives us the lexicographic order position of commit 2 I.e. 1.

Long story short, there is no issue regarding xor_offset. This xor_offset is not relative to the current commit. It is absolute.

Sorry for the initial claim :)

#5 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty
Quoted 9 lines · Abhradeep Chakraborty
Quoted 3 lines · Taylor Blau

Abhradeep -- do you have any thoughts about what this might be used for? I'll try to remember it myself, but I imagine that we could just as easily remove this altogether and avoid the confusion.

Honestly, I never understood the logic behind adding this flag option. I thought you have a reason to do that. Even I was thinking of curving it to 1 byte. I will remove it then.

I think removing it makes more sense. Since many of the other fields are 4-bytes wide, it's important for alignment purposes that those fields have addresses which are a multiple of four (relative to the start of the region, hence the 4-byte wide flags field).

But I'd just as soon get rid of it, so I think that makes sense to me.

Thanks,
Taylor

#9 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty
Quoted 11 lines · Abhradeep Chakraborty
Quoted 10 lines · Taylor Blau

I remember we had a brief off-list discussion about whether we should store the full object IDs in the offset table, or whether we could store their pack- or index-relative ordering. Is there a reason to prefer one or the other?

I don't think we need to explain the choice fully in the documentation in this patch, but it may be worth thinking about separately nonetheless. We can store either order and convert it to an object ID in constant time.

Quoted 7 lines
Quoted 3 lines

To figure out which is best, I would recommend trying a few different choices here and seeing how they do or don't impact your performance testing.

I think at that time I thought it would add extra cost of computing the actual commit ids from those index position. So, I didn't go further here.

It should be negligible relative to everything else, I would imagine. The function that converts an index position into an object ID is nth_packed_object_id().

Quoted 3 lines

I still have a feeling that there is some way to get rid of this list of commit ids. But at the same time, I do not want to add extra computation to the code.

I'm hoping that the additional complexity is minor. And if we can save some extra bytes that aren't necessary in the first place without compromising on performance, I think that's worthwhile to do.

Thanks,
Taylor

#13 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty
Quoted 7 lines · Abhradeep Chakraborty
Quoted 3 lines · Derrick Stolee

I think you mean 0x10 (b_1_0000) instead of 0xf (b_1111).

I noticed when looking at the constant in patch 2.

Yes, you're right. It's kind of embarrassment for me :)

It happens ;). Let's use 0x10 instead.

Thanks,
Taylor

#10 lore raw

Re: [PATCH 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Taylor Blau
Quoted 11 lines · Taylor Blau

It should be negligible relative to everything else, I would imagine. The function that converts an index position into an object ID is nth_packed_object_id().

Quoted 3 lines

I still have a feeling that there is some way to get rid of this list of commit ids. But at the same time, I do not want to add extra computation to the code.

I'm hoping that the additional complexity is minor. And if we can save some extra bytes that aren't necessary in the first place without compromising on performance, I think that's worthwhile to do.

Ok. I will look into it then.

Most of the reviews has been addressed. Hope I will be able to submit it soon.

Thanks :)

#19 lore raw

Re: [PATCH 2/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty
Quoted 14 lines · Abhradeep Chakraborty
Quoted 13 lines · Taylor Blau

Yeah. The problem here is that we can't record every commit that _doesn't_ have a bitmap every time we return NULL from one of these queries, since there are arbitrarily many such commits that don't have bitmaps.

We could approximate it using a Bloom filter or something, and much of that code is already written and could be interesting to try and reuse. But I wonder if we could get by with something simpler, though, which would cause us to load all bitmaps from the lookup table after a fixed number of cache misses (at which point we should force ourselves to load everything and just read everything out of a single O(1) lookup in the stored bitmap table).

Quoted 7 lines
Quoted 4 lines

That may or may not be a good idea, and the threshold will probably be highly dependent on the system. So it may not even be worth it, but I think it's an interesting area to experiemnt in and think a little more about.

Now I got the point. I wonder what if we leave it as it is. How much will it affect the code?

I'm not sure, and I think that it depends a lot on the repository and query that we're running.

I'd imagine that the effect is probably measurable, but small. Each hash lookup is cheap, but if there are many such lookups (a large proportion of which end up resulting in "no, we haven't loaded this bitmap yet" and then "...because no such bitmap exists for that commit") at some point it is worth it to fault all of the commits that _do_ have bitmaps in and answer authoritatively.

In other words, right now we have to do two queries when an commit doesn't have a bitmap stored:

- first, a lookup to see whether we have already loaded a bitmap for that commit

- then, a subsequent lookup to see whether the .bitmap file itself has a bitmap for that commit, but we just haven't loaded it yet

If we knew that we had loaded all of the bitmaps in the file, then we could simplify the above two queries into one, since whatever the first one returns is enough to know whether or not a bitmap exists at all.

Quoted 10 lines
Quoted 2 lines

How does this commit_pos work again? I confess I have forgetten since I wrote some of this code a while ago... :-).

It is using recursive strategy. The first call to stored_bitmap_for_commit function do not have pos_hint. So, it uses bitmap_table_lookup to find the commit position in the list and makes a call to lazy_bitmap_for_commit function. This function gets the offset and xor-offset using the commit id's position in the list. If xor-offset exists, it is using this xor-offset to get the xor-bitmap by calling stored_bitmap_for_commit again. But this time pos_hint is xor-offset. This goes on till the last non-xor bitmap has found.

Ahhh. Thanks for refreshing my memory. I wonder if you think there is a convenient way to work some of this into a short comment to help other readers in the future, too.

Quoted 2 lines

As I said before, xor-offset should be an absolute value to make it work correctly.

Yep, makes sense.

Quoted 9 lines
Quoted 2 lines

Should we print this regardless of whether or not there is a lookup table? We should be able to learn the entry count either way.

No, this is necessary. "Bitmap v1 test (%d entries loaded)" means all the bitmap entries has been loaded. It is basically for load_bitmap_entries_bitmap_v1 function which loads all the bitmaps One by one. But if there is a lookup table, prepare_bitmap_git function will not load every entries and thus printing the above line is wrong.

Right, that part makes sense to me. But I wonder if we should still print something, perhaps just "Bitmap v1 test" or "Bitmap v1 test (%d entries)" omitting the "loaded" part.

Thanks,
Taylor

#25 lore raw

Re: [PATCH 3/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty
Quoted 37 lines · Abhradeep Chakraborty
Quoted 2 lines · Taylor Blau

I'm not sure if I remember why table[i] - selected->xor_offset is right and not i - selected->xor_offset.

Even I myself got confused! Before sending the patch to the mailing list, I was clear about that. That's why I didn't catch the so called mistake I have been notifying till now. Thanks Taylor for asking the question!

I should add a comment before the line so that people can understand it. Let us parse table_inv[table[i] - selected->xor_offset] -

Suppose bitmap entries be like -

Bitmap 0 (for commit 0)
Bitmap 1 (for commit 1)
Bitmap 2 (for commit 2)
Bitmap 3 (for commit 3)
.
.
.
Bitmap 20 (for commit 20)

These bitmaps are ordered by the date of their corresponding commit. table array maps commit's lexicographic order to its bitmap order. table_inv stores the reverse (i.e. it maps bitmap order to lexicographic order). Say for example, if commit 4 is lexicographically first among all the Commits then table[0] is 4. Similarly table[1]=2, table[2]=1 etc. table_inv[4] is 0, table_inv[2]=1 etc.

Now suppose commit 4's bitmap has xor-relation with commit 2's bitmap. So, xor-offset for bitmap 4 is 2. And table[0] - selected->xor_offset is equal to 4-2 = 2. It is pointing to the commit 2. Now, 2 is in bitmap Order. We need to convert it into lexicographic order. So, table_inv[2] gives us the lexicographic order position of commit 2 I.e. 1.

Quoted 4 lines

Long story short, there is no issue regarding xor_offset. This xor_offset is not relative to the current commit. It is absolute.

Sorry for the initial claim :)

Ahhhhh. Makes perfect sense. Thanks!

Thanks,
Taylor

#29 lore raw

Re: [PATCH 5/6] bitmap-commit-table: add tests for the bitmap lookup table

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 17 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add tests to check the working of the newly implemented lookup table.

---
 t/t5310-pack-bitmaps.sh       | 14 ++++++++++++++
 t/t5326-multi-pack-bitmaps.sh | 19 +++++++++++++++++++
 2 files changed, 33 insertions(+)
t/t5310-pack-bitmaps.sh +0 -0
index f775fc1ce69..f05d3e6ace7 100755

t/t5310-pack-bitmaps.sh test_expect_success 'full repack creates bitmaps' ' · line 49

Quoted 7 lines
t/t5310-pack-bitmaps.sh +4 -0
@@ -43,6 +43,20 @@ test_expect_success 'full repack creates bitmaps' '
 
 basic_bitmap_tests
 
+test_expect_success 'using lookup table does not affect basic bitmap tests' '
+	test_config pack.writeBitmapLookupTable true &&
+	git repack -adb
+'

Whether or not we end up making pack.writeBitmapLookupTable be "true" by default, I wonder if we should just set it to "true" whenever we write a bitmap in this file, and then adjust whether or not we *read* the lookup table with the GIT_TEST_ environment variable you introduced a few commits back.

Thinking on it more, though, I don't think it makes a huge practical difference for the code here in "t", since these repositories are tiny and repacking them or rewriting their bitmaps is cheap.

But in the performance tests it probably makes a bigger difference.

Quoted 18 lines
t/t5310-pack-bitmaps.sh +10 -0
+basic_bitmap_tests
+
+test_expect_success 'using lookup table does not let each entries to be parsed one by one' '
+	test_config pack.writeBitmapLookupTable true &&
+	git repack -adb &&
+	git rev-list --test-bitmap HEAD 2>out &&
+	grep "Found bitmap for" out &&
+	! grep "Bitmap v1 test "
+'
+
 test_expect_success 'incremental repack fails when bitmaps are requested' '
 	test_commit more-1 &&
 	test_must_fail git repack -d 2>err &&
t/t5326-multi-pack-bitmaps.sh +0 -0
index 4fe57414c13..85fbdf5e4bb 100755

t/t5326-multi-pack-bitmaps.sh test_expect_success 'graceful fallback when missing reverse index' ' · line 315

Quoted 10 lines
t/t5326-multi-pack-bitmaps.sh +7 -0
@@ -306,5 +306,24 @@ test_expect_success 'graceful fallback when missing reverse index' '
 		! grep "ignoring extra bitmap file" err
 	)
 '
+test_expect_success 'multi-pack-index write --bitmap writes lookup table if enabled' '
+	rm -fr repo &&
+	git init repo &&
+	test_when_finished "rm -fr repo" &&
+	(
+		cd repo &&
+		test_commit_bulk 106 &&

Is there a reason we need to write this many commits? I think this is copied from a test further up which deals explicitly with a case where there are too many commits to write bitmaps for all of them (hence we need to write more commits than 100 or so).

But I think for our purposes here we just need a single commit, written into a single pack, which is covered with a MIDX.

So it should suffice to do something like:

    test_commit base &&
    git repack -ad &&
    git config pack.writeBitmapLookupTable true &&
    git multi-pack-index write --bitmap &&
    [...]

instead of what's written here.

Thanks,
Taylor

#31 lore raw

Re: [PATCH 6/6] bitmap-lookup-table: add performance tests

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 3 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add performance tests for bitmap lookup table extension.

These tests look good, though I left a few notes below which boil down to recommending a separate commit to set pack.writeReverseIndex=true, and some suggestions for how to clean up the diff in the two performance scripts you modified.

I would be interested to see the relevant results from running these perf scripts on a reasonably large-sized repository, e.g. the kernel or similar.

For the next version of this series, would you mind running these scripts and including the results in this commit message?

Quoted 13 lines
---
 t/perf/p5310-pack-bitmaps.sh       | 60 +++++++++++++++++++-----------
 t/perf/p5326-multi-pack-bitmaps.sh | 55 +++++++++++++++++----------
 2 files changed, 73 insertions(+), 42 deletions(-)
t/perf/p5310-pack-bitmaps.sh +0 -0
index 7ad4f237bc3..a8d9414de92 100755

t/perf/p5310-pack-bitmaps.sh test_perf_large_repo · line 17

Quoted 10 lines
t/perf/p5310-pack-bitmaps.sh +3 -2
@@ -10,10 +10,11 @@ test_perf_large_repo
 # since we want to be able to compare bitmap-aware
 # git versus non-bitmap git
 #
-# We intentionally use the deprecated pack.writebitmaps
+# We intentionally use the deprecated pack.writeBitmaps
 # config so that we can test against older versions of git.
 test_expect_success 'setup bitmap config' '
-	git config pack.writebitmaps true
+	git config pack.writeBitmaps true &&
+	git config pack.writeReverseIndex true

I suspect that eliminating the overhead of generating the reverse index in memory is important to see the effect of this test. We should make sure that this is done in a separate step so when we compare two commits that both have a reverse index written.

That being said, we should probably make reverse indexes be the default anyways, since they help significantly with all kinds of things (really, any operation which has to generate a reverse index in memory, like preparing a pack to push, the '%(objectsize:disk)' cat-file formatting atom, and so on.

So at a minimum I would suggest extracting a separate commit here which sets pack.writeReverseIndex to true for this test. That way the commit prior to this has reverse indexes written, and comparing "this commit" to "the previous one" is isolating the effect of just the lookup table.

But as a useful sideproject, it would be worthwhile to investigate setting this to true by default everywhere, perhaps after this series has settled a little more (or if you are blocked / want something else to do).

Quoted 19 lines
t/perf/p5310-pack-bitmaps.sh +0 -12
 '
 
 # we need to create the tag up front such that it is covered by the repack and
@@ -28,27 +29,42 @@ test_perf 'repack to disk' '
 
 test_full_bitmap
 
-test_expect_success 'create partial bitmap state' '
-	# pick a commit to represent the repo tip in the past
-	cutoff=$(git rev-list HEAD~100 -1) &&
-	orig_tip=$(git rev-parse HEAD) &&
-
-	# now kill off all of the refs and pretend we had
-	# just the one tip
-	rm -rf .git/logs .git/refs/* .git/packed-refs &&
-	git update-ref HEAD $cutoff &&
-
-	# and then repack, which will leave us with a nice
-	# big bitmap pack of the "old" history, and all of

t/perf/p5310-pack-bitmaps.sh test_perf 'repack to disk' ' · line 34

Quoted 10 lines
t/perf/p5310-pack-bitmaps.sh +2 -7
-	# the new history will be loose, as if it had been pushed
-	# up incrementally and exploded via unpack-objects
-	git repack -Ad &&
-
-	# and now restore our original tip, as if the pushes
-	# had happened
-	git update-ref HEAD $orig_tip
+test_perf 'use lookup table' '
+    git config pack.writeBitmapLookupTable true
 '

This part doesn't need to use 'test_perf', since we don't care about the performance of running "git config". Instead, using test_expect_success is more appropriate here.

Quoted 24 lines
t/perf/p5310-pack-bitmaps.sh +23 -1
-test_partial_bitmap
+test_perf 'repack to disk (lookup table)' '
+    git repack -adb
+'
+
+test_full_bitmap
+
+for i in false true
+do
+	$i && lookup=" (lookup table)"
+	test_expect_success "create partial bitmap state$lookup" '
+		git config pack.writeBitmapLookupTable '"$i"' &&
+		# pick a commit to represent the repo tip in the past
+		cutoff=$(git rev-list HEAD~100 -1) &&
+		orig_tip=$(git rev-parse HEAD) &&
+
+		# now kill off all of the refs and pretend we had
+		# just the one tip
+		rm -rf .git/logs .git/refs/* .git/packed-refs &&
+		git update-ref HEAD $cutoff &&
+
+		# and then repack, which will leave us with a nice
+		# big bitmap pack of the "old" history, and all of
+		# the new history will be loose, as if it had been pushed

t/perf/p5310-pack-bitmaps.sh test_perf 'repack to disk' ' · line 68

Quoted 10 lines
t/perf/p5310-pack-bitmaps.sh +10 -0
+		# up incrementally and exploded via unpack-objects
+		git repack -Ad &&
+
+		# and now restore our original tip, as if the pushes
+		# had happened
+		git update-ref HEAD $orig_tip
+	'
+
+	test_partial_bitmap
+done

Could we extract the body of this loop into a function whose first argument is either true/false? I think that would improve readability here, and potentially clean up the diff a little bit.

For what it's worth, I don't think we need to do anything fancier for the test name other than:

    test_partial_bitmap () {
      local enabled="$1"
      test_expect_success "create partial bitmap state (lookup=$enabled)" '
        git config pack.writeBitmapLookupTable "$enabled" &&
        [...]
      '
    }

    test_partial_bitmap false
    test_partial_bitmap true

or something.

Quoted 17 lines
t/perf/p5310-pack-bitmaps.sh +17 -0
+for i in false true
+do
+	$i && lookup=" (lookup table)"
+	test_expect_success "create partial bitmap state$lookup" '
+		git config pack.writeBitmapLookupTable '"$i"' &&
+		# pick a commit to represent the repo tip in the past
+		cutoff=$(git rev-list HEAD~100 -1) &&
+		orig_tip=$(git rev-parse HEAD) &&
+
+		# now pretend we have just one tip
+		rm -rf .git/logs .git/refs/* .git/packed-refs &&
+		git update-ref HEAD $cutoff &&
+
+		# and then repack, which will leave us with a nice
+		# big bitmap pack of the "old" history, and all of
+		# the new history will be loose, as if it had been pushed
+		# up incrementally and exploded via unpack-objects

t/perf/p5310-pack-bitmaps.sh test_perf 'repack to disk' '

Quoted 10 lines
t/perf/p5310-pack-bitmaps.sh +10 -0
+		git repack -Ad &&
+		git multi-pack-index write --bitmap &&
+
+		# and now restore our original tip, as if the pushes
+		# had happened
+		git update-ref HEAD $orig_tip
+	'
+
+	test_partial_bitmap
+done

Same note here.

Thanks,
Taylor

#20 lore raw

Re: [PATCH 2/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau
Quoted 6 lines · Taylor Blau

In other words, right now we have to do two queries when an commit doesn't have a bitmap stored:

- first, a lookup to see whether we have already loaded a bitmap for that commit

Quoted 6 lines

- then, a subsequent lookup to see whether the .bitmap file itself has a bitmap for that commit, but we just haven't loaded it yet

If we knew that we had loaded all of the bitmaps in the file, then we could simplify the above two queries into one, since whatever the first one returns is enough to know whether or not a bitmap exists at all.

Hmm, agreed.

Quoted 3 lines

Ahhh. Thanks for refreshing my memory. I wonder if you think there is a convenient way to work some of this into a short comment to help other readers in the future, too.

Actually, Derrick has suggested to go with iterative approach[1] instead of Recursive approach. What's your view on it?

Quoted 3 lines

Right, that part makes sense to me. But I wonder if we should still print something, perhaps just "Bitmap v1 test" or "Bitmap v1 test (%d entries)" omitting the "loaded" part.

Yeah, of course we can!

Thanks :)

[1] https://lore.kernel.org/git/[email protected]/

#21 lore raw

Re: [PATCH 2/6] pack-bitmap: prepare to read lookup table extension

Quoted 6 lines · Abhradeep Chakraborty
Quoted 3 lines

Ahhh. Thanks for refreshing my memory. I wonder if you think there is a convenient way to work some of this into a short comment to help other readers in the future, too.

Actually, Derrick has suggested to go with iterative approach[1] instead of Recursive approach. What's your view on it?

I don't have a strong feeling about it. In practice, we seem to top out at ~500 bitmaps or so for large-ish repositories, so I would be surprised to see this result in stack exhaustion even in the worst case (every bitmap xor'd with the previous one, forming a long chain).

But it doesn't hurt to be defensive, so I think it's worth it as long as you don't find the implementation too complex.

Quoted 1 line

[1] https://lore.kernel.org/git/[email protected]/

Thanks,
Taylor

#33 lore raw

PATCH v2 1/6 Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx or pack, along with their offset and xor offset. This way git can load only the neccesary bitmaps without loading the previous bitmaps.

The older version of Git ignores the lookup table extension and doesn't throw any kind of warning or error while parsing the bitmap file.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

---
 Documentation/technical/bitmap-format.txt | 41 +++++++++++++++++++++++
 1 file changed, 41 insertions(+)
Documentation/technical/bitmap-format.txt +41 -0
index 04b3ec21785..7d4e450d3d8 100644
@@ -67,6 +67,19 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` <commit pos, offset, xor offset>
+			triplets. The format and meaning of the table is described
+			below.
++
+NOTE: This xor_offset is different from the bitmap's xor_offset.
+Bitmap's xor_offset is relative i.e. it tells how many bitmaps we have
+to go back from the current bitmap. Lookup table's xor_offset tells the
+position of the triplet in the list whose bitmap the current commit's
+bitmap have to xor with.
+
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +218,31 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last `N * (4 + 8 + 4)`
+(preceding the name-hash cache and trailing hash) of the `.bitmap` file
+contains a lookup table specifying the information needed to get the
+desired bitmap from the entries without parsing previous unnecessary
+bitmaps.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the table
+contains a list of `nr_entries` <commit pos, offset, xor offset> triplets.
+The content of i'th triplet is -
+
+	* {empty}
+	commit pos (4 byte integer, network byte order): ::
+	It stores the object position of the commit (in the midx or pack index)
+	to which the i'th bitmap in the bitmap entries belongs.
+
+	* {empty}
+	offset (8 byte integer, network byte order): ::
+	The offset from which that commit's bitmap can be read.
+
+	* {empty}
+	xor offset (4 byte integer, network byte order): ::
+	It holds the position of the triplet with whose bitmap the
+	current bitmap need to xor. If the current triplet's bitmap
+	do not have any xor bitmap, it defaults to 0xffffffff.
Signature · 1 line
gitgitgadget
#37 lore raw

PATCH v2 2/6 pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documentated by an earlier change, but Git does not yet knowhow to write that extension.

Teach git to write bitmap lookup table extension. The table contains the list of N <commit pos, offset, xor offset>` triplets. These triplets are sorted according to their commit pos (ascending order). The meaning of each data in the i'th triplet is given below:

- Commit pos is the position of the commit in the pack-index (or midx) to which the i'th bitmap belongs. It is a 4 byte network byte order integer.

- offset is the position of the i'th bitmap.

- xor offset denotes the position of the triplet with whose bitmap the current triplet's bitmap need to xor with.

---
 pack-bitmap-write.c | 72 +++++++++++++++++++++++++++++++++++++++++++--
 pack-bitmap.h       |  5 ++--
 2 files changed, 73 insertions(+), 4 deletions(-)
pack-bitmap-write.c +70 -2
index c43375bd344..899a4a941e1 100644
@@ -650,7 +650,9 @@ static const struct object_id *oid_access(size_t pos, const void *table)
 
 static void write_selected_commits_v1(struct hashfile *f,
 				      struct pack_idx_entry **index,
-				      uint32_t index_nr)
+				      uint32_t index_nr,
+				      uint64_t *offsets,
+				      uint32_t *commit_positions)
 {
 	int i;
 
@@ -663,6 +665,11 @@ static void write_selected_commits_v1(struct hashfile *f,
 		if (commit_pos < 0)
 			BUG("trying to write commit not in index");
 
+		if (offsets)
+			offsets[i] = hashfile_total(f);
+		if (commit_positions)
+			commit_positions[i] = commit_pos;
+
 		hashwrite_be32(f, commit_pos);
 		hashwrite_u8(f, stored->xor_offset);
 		hashwrite_u8(f, stored->flags);
@@ -671,6 +678,55 @@ static void write_selected_commits_v1(struct hashfile *f,
 	}
 }
 
+static int table_cmp(const void *_va, const void *_vb, void *commit_positions)
+{
+	int8_t result = 0;
+	uint32_t *positions = (uint32_t *) commit_positions;
+	uint32_t a = positions[*(uint32_t *)_va];
+	uint32_t b = positions[*(uint32_t *)_vb];
+
+	if (a > b)
+		result = 1;
+	else if (a < b)
+		result = -1;
+	else
+		result = 0;
+
+	return result;
+}
+
+static void write_lookup_table(struct hashfile *f,
+			       uint64_t *offsets,
+			       uint32_t *commit_positions)
+{
+	uint32_t i;
+	uint32_t *table, *table_inv;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;
+
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		uint32_t xor_offset = selected->xor_offset;
+
+		hashwrite_be32(f, commit_positions[table[i]]);
+		hashwrite_be64(f, offsets[table[i]]);
+		hashwrite_be32(f, xor_offset ?
+				table_inv[table[i] - xor_offset]: 0xffffffff);
+	}
+
+	free(table);
+	free(table_inv);
+}
+
 static void write_hash_cache(struct hashfile *f,
 			     struct pack_idx_entry **index,
 			     uint32_t index_nr)
@@ -695,6 +751,8 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 {
 	static uint16_t default_version = 1;
 	static uint16_t flags = BITMAP_OPT_FULL_DAG;
+	uint64_t *offsets = NULL;
+	uint32_t *commit_positions = NULL;
 	struct strbuf tmp_file = STRBUF_INIT;
 	struct hashfile *f;
 
@@ -715,8 +773,16 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 	dump_bitmap(f, writer.trees);
 	dump_bitmap(f, writer.blobs);
 	dump_bitmap(f, writer.tags);
-	write_selected_commits_v1(f, index, index_nr);
 
+	if (options & BITMAP_OPT_LOOKUP_TABLE) {
+		CALLOC_ARRAY(offsets, index_nr);
+		CALLOC_ARRAY(commit_positions, index_nr);
+	}
+
+	write_selected_commits_v1(f, index, index_nr, offsets, commit_positions);
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		write_lookup_table(f, offsets, commit_positions);
 	if (options & BITMAP_OPT_HASH_CACHE)
 		write_hash_cache(f, index, index_nr);
 
@@ -730,4 +796,6 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 		die_errno("unable to rename temporary bitmap file to '%s'", filename);
 
 	strbuf_release(&tmp_file);
+	free(offsets);
+	free(commit_positions);
 }
pack-bitmap.h +3 -2
index 3d3ddd77345..67a9d0fc303 100644
@@ -24,8 +24,9 @@ struct bitmap_disk_header {
 #define NEEDS_BITMAP (1u<<22)
 
 enum pack_bitmap_opts {
-	BITMAP_OPT_FULL_DAG = 1,
-	BITMAP_OPT_HASH_CACHE = 4,
+	BITMAP_OPT_FULL_DAG = 0x1,
+	BITMAP_OPT_HASH_CACHE = 0x4,
+	BITMAP_OPT_LOOKUP_TABLE = 0x10,
 };
 
 enum pack_bitmap_flags {
Signature · 1 line
gitgitgadget
#43 lore raw

PATCH v2 3/6 pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Teach git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'. Default is true.

Also add test to verify writting of lookup table.

---
 Documentation/config/pack.txt |  7 +++++++
 builtin/multi-pack-index.c    |  8 ++++++++
 builtin/pack-objects.c        | 10 +++++++++-
 midx.c                        |  3 +++
 midx.h                        |  1 +
 pack-bitmap-write.c           |  2 ++
 t/t5310-pack-bitmaps.sh       |  3 ++-
 t/t5326-multi-pack-bitmaps.sh | 13 +++++++++++++
 8 files changed, 45 insertions(+), 2 deletions(-)
Documentation/config/pack.txt +7 -0
index ad7f73a1ead..6e1f454c4d6 100644
@@ -164,6 +164,13 @@ When writing a multi-pack reachability bitmap, no new namehashes are
 computed; instead, any namehashes stored in an existing bitmap are
 permuted into their appropriate location when writing a new bitmap.
 
+pack.writeBitmapLookupTable::
+	When true, git will include a "lookup table" section in the
+	bitmap index (if one is written). This table is used to defer
+	loading individual bitmaps as late as possible. This can be
+	beneficial in repositories which have relatively large bitmap
+	indexes. Defaults to true.
+
 pack.writeReverseIndex::
 	When true, git will write a corresponding .rev file (see:
 	link:../technical/pack-format.html[Documentation/technical/pack-format.txt])
builtin/multi-pack-index.c +8 -0
index 5edbb7fe86e..3757616f09c 100644
@@ -87,6 +87,13 @@ static int git_multi_pack_index_write_config(const char *var, const char *value,
 			opts.flags &= ~MIDX_WRITE_BITMAP_HASH_CACHE;
 	}
 
+	if (!strcmp(var, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(var, value))
+			opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+		else
+			opts.flags &= ~MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+	}
+
 	/*
 	 * We should never make a fall-back call to 'git_default_config', since
 	 * this was already called in 'cmd_multi_pack_index()'.
@@ -123,6 +130,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv)
 	};
 
 	opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE;
+	opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
 
 	git_config(git_multi_pack_index_write_config, NULL);
 
builtin/pack-objects.c +9 -1
index 39e28cfcafc..d6a33fd486c 100644
@@ -228,7 +228,7 @@ static enum {
 	WRITE_BITMAP_QUIET,
 	WRITE_BITMAP_TRUE,
 } write_bitmap_index;
-static uint16_t write_bitmap_options = BITMAP_OPT_HASH_CACHE;
+static uint16_t write_bitmap_options = BITMAP_OPT_HASH_CACHE | BITMAP_OPT_LOOKUP_TABLE;
 
 static int exclude_promisor_objects;
 
@@ -3148,6 +3148,14 @@ static int git_pack_config(const char *k, const char *v, void *cb)
 		else
 			write_bitmap_options &= ~BITMAP_OPT_HASH_CACHE;
 	}
+
+	if (!strcmp(k, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(k, v))
+			write_bitmap_options |= BITMAP_OPT_LOOKUP_TABLE;
+		else
+			write_bitmap_options &= ~BITMAP_OPT_LOOKUP_TABLE;
+	}
+
 	if (!strcmp(k, "pack.usebitmaps")) {
 		use_bitmap_index_default = git_config_bool(k, v);
 		return 0;
midx.c +3 -0
index 5f0dd386b02..9c26d04bfde 100644
@@ -1072,6 +1072,9 @@ static int write_midx_bitmap(char *midx_name, unsigned char *midx_hash,
 	if (flags & MIDX_WRITE_BITMAP_HASH_CACHE)
 		options |= BITMAP_OPT_HASH_CACHE;
 
+	if (flags & MIDX_WRITE_BITMAP_LOOKUP_TABLE)
+		options |= BITMAP_OPT_LOOKUP_TABLE;
+
 	prepare_midx_packing_data(&pdata, ctx);
 
 	commits = find_commits_for_midx_bitmap(&commits_nr, refs_snapshot, ctx);
midx.h +1 -0
index 22e8e53288e..5578cd7b835 100644
@@ -47,6 +47,7 @@ struct multi_pack_index {
 #define MIDX_WRITE_REV_INDEX (1 << 1)
 #define MIDX_WRITE_BITMAP (1 << 2)
 #define MIDX_WRITE_BITMAP_HASH_CACHE (1 << 3)
+#define MIDX_WRITE_BITMAP_LOOKUP_TABLE (1 << 4)
 
 const unsigned char *get_midx_checksum(struct multi_pack_index *m);
 void get_midx_filename(struct strbuf *out, const char *object_dir);
pack-bitmap-write.c +2 -0
index 899a4a941e1..79be0cf80e6 100644
@@ -713,6 +713,7 @@ static void write_lookup_table(struct hashfile *f,
 	for (i = 0; i < writer.selected_nr; i++)
 		table_inv[table[i]] = i;
 
+	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
 	for (i = 0; i < writer.selected_nr; i++) {
 		struct bitmapped_commit *selected = &writer.selected[table[i]];
 		uint32_t xor_offset = selected->xor_offset;
@@ -725,6 +726,7 @@ static void write_lookup_table(struct hashfile *f,
 
 	free(table);
 	free(table_inv);
+	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
 }
 
 static void write_hash_cache(struct hashfile *f,
t/t5310-pack-bitmaps.sh +2 -1
index f775fc1ce69..c669ed959e9 100755
@@ -38,7 +38,8 @@ test_expect_success 'full repack creates bitmaps' '
 	ls .git/objects/pack/ | grep bitmap >output &&
 	test_line_count = 1 output &&
 	grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace &&
-	grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace
+	grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace &&
+	grep "\"label\":\"writing_lookup_table\"" trace
 '
 
 basic_bitmap_tests
t/t5326-multi-pack-bitmaps.sh +13 -0
index 4fe57414c13..43be49617b8 100755
@@ -307,4 +307,17 @@ test_expect_success 'graceful fallback when missing reverse index' '
 	)
 '
 
+test_expect_success 'multi-pack-index write writes lookup table if enabled' '
+	rm -fr repo &&
+	git init repo &&
+	test_when_finished "rm -fr repo" &&
+	(
+		cd repo &&
+		test_commit base &&
+		git repack -ad &&
+		GIT_TRACE2_EVENT="$(pwd)/trace" \
+			git multi-pack-index write --bitmap &&
+		grep "\"label\":\"writing_lookup_table\"" trace
+	)
+'
 test_done
Signature · 1 line
gitgitgadget
#32 lore raw

v2 cover bitmap: integrate a lookup table extension to the bitmap format

In reply to Abhradeep Chakraborty via GitGitGadget

When parsing the .bitmap file, git loads all the bitmaps one by one even if some of the bitmaps are not necessary. We can remove this overhead by loading only the necessary bitmaps. A look up table extension can solve this issue.

Changes since v1:

This is the second version which addressed all (I think) the reviews. Please notify me if some reviews are not addressed :)

* The table size is decreased and the format has also changed. It now contains nr_entries triplets of size 4+8+4 bytes. Each triplet contains the following things - (1) 4 byte commit position (in the pack-index or midx) (2) 8 byte offset and (3) 4 byte xor triplet (i.e. with whose bitmap the current triplet's bitmap has to xor) position.
* Performance tests are splitted into two commits. First contains the actual performance tests and second enables the pack.writeReverseIndex (as suggested by Taylor).
* st_*() functions are used.
* commit order is changed according to Derrick's suggestion.
* Iterative approach is used instead of recursive approach to parse xor bitmaps. (As suggested by Derrick).
* Some minor bug fixes of previous version.

Initial version:

The proposed table has:

* a list of nr_entries object ids. These objects are commits that has bitmaps. Ids are stored in lexicographic order (for better searching).
* a list of <offset, xor-offset> pairs (4-byte integers, network-byte order). The i'th pair denotes the offset and xor-offset(respectively) of the bitmap of i'th commit in the previous list. These two informations are necessary because only in this way bitmaps can be found without parsing all the bitmap.
* a 4-byte integer for table specific flags (none exists currently).

Whenever git want to parse the bitmap for a specific commit, it will first refer to the table and will look for the offset and xor-offset for that commit. Git will then try to parse the bitmap located at the offset position. The xor-offset can be used to find the xor-bitmap for the bitmap(if any).

Abhradeep Chakraborty (6):
Documentation/technical: describe bitmap lookup table extension pack-bitmap-write.c: write lookup table extension
pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
pack-bitmap: prepare to read lookup table extension
bitmap-lookup-table: add performance tests for lookup table p5310-pack-bitmaps.sh: enable pack.writeReverseIndex for testing

 Documentation/config/pack.txt             |   7 +
 Documentation/technical/bitmap-format.txt |  41 +++++
 builtin/multi-pack-index.c                |   8 +
 builtin/pack-objects.c                    |  10 +-
 midx.c                                    |   3 +
 midx.h                                    |   1 +
 pack-bitmap-write.c                       |  74 ++++++++-
 pack-bitmap.c                             | 193 ++++++++++++++++++++--
 pack-bitmap.h                             |   5 +-
 t/perf/p5310-pack-bitmaps.sh              |  66 ++++----
 t/perf/p5326-multi-pack-bitmaps.sh        |  93 ++++++-----
 t/t5310-pack-bitmaps.sh                   |  10 +-
 t/t5326-multi-pack-bitmaps.sh             |  14 ++
 13 files changed, 439 insertions(+), 86 deletions(-)

base-commit: 39c15e485575089eb77c769f6da02f98a55905e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1266%2FAbhra303%2Fbitmap-commit-table-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1266/Abhra303/bitmap-commit-table-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/1266

Range-diff vs v1:

1: 2e22ca5069a ! 1: 4d11be66cfa Documentation/technical: describe bitmap lookup table extension
@@ Commit message
When reading bitmap file, git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading
- bitmaps which stores a list of bitmapped commit oids, along with their
- offset and xor offset. This way git can load only the neccesary bitmaps
- without loading the previous bitmaps.
+ bitmaps which stores a list of bitmapped commit id pos (in the midx
+ or pack, along with their offset and xor offset. This way git can
+ load only the neccesary bitmaps without loading the previous bitmaps.
+
+ The older version of Git ignores the lookup table extension and doesn't
+ throw any kind of warning or error while parsing the bitmap file.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

- Co-Authored-by: Taylor Blau <[email protected]>
- Mentored-by: Taylor Blau <[email protected]>
+ Co-Authored-by: Taylor Blau <[email protected]>
+ Mentored-by: Taylor Blau <[email protected]>

@@ Documentation/technical/bitmap-format.txt: MIDXs, both the bit-cache and rev-cac
described below.

+ ** {empty}
-+ BITMAP_OPT_LOOKUP_TABLE (0xf) : :::
++ BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+ If present, the end of the bitmap file contains a table
-+ containing a list of N object ids, a list of pairs of
-+ offset and xor offset of respective objects, and 4-byte
-+ integer denoting the flags (currently none). The format
-+ and meaning of the table is described below.
++ containing a list of N <commit pos, offset, xor offset>
++ triplets. The format and meaning of the table is described
++ below.
+++
++NOTE: This xor_offset is different from the bitmap's xor_offset. ++Bitmap's xor_offset is relative i.e. it tells how many bitmaps we have ++to go back from the current bitmap. Lookup table's xor_offset tells the ++position of the triplet in the list whose bitmap the current commit's ++bitmap have to xor with.
+
4-byte entry count (network byte order)

@@ Documentation/technical/bitmap-format.txt: Note that this hashing scheme is tied
+Commit lookup table
+-------------------
+
-+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the end of the .bitmap -+contains a lookup table specifying the positions of commits which have a -+bitmap.
++If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last N * (4 + 8 + 4) ++(preceding the name-hash cache and trailing hash) of the .bitmap file ++contains a lookup table specifying the information needed to get the ++desired bitmap from the entries without parsing previous unnecessary ++bitmaps.
+
-+For a .bitmap containing nr_entries reachability bitmaps, the format -+is as follows:
++For a .bitmap containing nr_entries reachability bitmaps, the table ++contains a list of nr_entries <commit pos, offset, xor offset> triplets. ++The content of i'th triplet is -
+
-+ - nr_entries object names.
++ * {empty}
++ commit pos (4 byte integer, network byte order): ::
++ It stores the object position of the commit (in the midx or pack index)
++ to which the i'th bitmap in the bitmap entries belongs.
+
-+ - nr_entries pairs of 4-byte integers, each in network order.
-+ The first holds the offset from which that commit's bitmap can
-+ be read. The second number holds the position of the commit
-+ whose bitmap the current bitmap is xor'd with in lexicographic
-+ order, or 0xffffffff if the current commit is not xor'd with
-+ anything.
++ * {empty}
++ offset (8 byte integer, network byte order): ::
++ The offset from which that commit's bitmap can be read.
+
-+ - One 4-byte network byte order integer specifying
-+ table-specific flags. None exist currently, so this is always
-+ "0".
++ * {empty}
++ xor offset (4 byte integer, network byte order): ::
++ It holds the position of the triplet with whose bitmap the
++ current bitmap need to xor. If the current triplet's bitmap
++ do not have any xor bitmap, it defaults to 0xffffffff.
3: ed91ebf69a8 ! 2: d118f1d45e6 pack-bitmap-write.c: write lookup table extension

     @@ Metadata
       ## Commit message ##
          pack-bitmap-write.c: write lookup table extension
      
     -    Teach git to write bitmap lookup table extension. The table has the
     -    following information:
     +    The bitmap lookup table extension was documentated by an earlier
     +    change, but Git does not yet knowhow to write that extension.
      
     -        - `N` no of Object ids of each bitmapped commits
     +    Teach git to write bitmap lookup table extension. The table contains
     +    the list of `N` <commit pos, offset, xor offset>` triplets. These
     +    triplets are sorted according to their commit pos (ascending order).
     +    The meaning of each data in the i'th triplet is given below:
      
     -        - A list of offset, xor-offset pair; the i'th pair denotes the
     -          offsets and xor-offsets of i'th commit in the previous list.
     +      - Commit pos is the position of the commit in the pack-index
     +        (or midx) to which the i'th bitmap belongs. It is a 4 byte
     +        network byte order integer.
      
     -        - 4-byte integer denoting the flags
     +      - offset is the position of the i'th bitmap.
      
     -    Co-authored-by: Taylor Blau <[email protected]>
     -    Mentored-by: Taylor Blau <[email protected]>
     +      - xor offset denotes the position of the triplet with whose
     +        bitmap the current triplet's bitmap need to xor with.
     +
     +    Co-authored-by: Taylor Blau <[email protected]>
     +    Mentored-by: Taylor Blau <[email protected]>
          Co-mentored-by: Kaartic Sivaraam <[email protected]>
          Signed-off-by: Abhradeep Chakraborty <[email protected]>
      
     @@ pack-bitmap-write.c: static const struct object_id *oid_access(size_t pos, const
       				      struct pack_idx_entry **index,
      -				      uint32_t index_nr)
      +				      uint32_t index_nr,
     -+				      off_t *offsets)
     ++				      uint64_t *offsets,
     ++				      uint32_t *commit_positions)
       {
       	int i;
       
     @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
       
      +		if (offsets)
      +			offsets[i] = hashfile_total(f);
     ++		if (commit_positions)
     ++			commit_positions[i] = commit_pos;
      +
       		hashwrite_be32(f, commit_pos);
       		hashwrite_u8(f, stored->xor_offset);
     @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
       	}
       }
       
     -+static int table_cmp(const void *_va, const void *_vb)
     ++static int table_cmp(const void *_va, const void *_vb, void *commit_positions)
      +{
     -+	return oidcmp(&writer.selected[*(uint32_t*)_va].commit->object.oid,
     -+		      &writer.selected[*(uint32_t*)_vb].commit->object.oid);
     ++	int8_t result = 0;
     ++	uint32_t *positions = (uint32_t *) commit_positions;
     ++	uint32_t a = positions[*(uint32_t *)_va];
     ++	uint32_t b = positions[*(uint32_t *)_vb];
     ++
     ++	if (a > b)
     ++		result = 1;
     ++	else if (a < b)
     ++		result = -1;
     ++	else
     ++		result = 0;
     ++
     ++	return result;
      +}
      +
      +static void write_lookup_table(struct hashfile *f,
     -+			       off_t *offsets)
     ++			       uint64_t *offsets,
     ++			       uint32_t *commit_positions)
      +{
      +	uint32_t i;
     -+	uint32_t flags = 0;
      +	uint32_t *table, *table_inv;
      +
      +	ALLOC_ARRAY(table, writer.selected_nr);
     @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
      +
      +	for (i = 0; i < writer.selected_nr; i++)
      +		table[i] = i;
     -+	QSORT(table, writer.selected_nr, table_cmp);
     ++
     ++	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);
     ++
      +	for (i = 0; i < writer.selected_nr; i++)
      +		table_inv[table[i]] = i;
      +
      +	for (i = 0; i < writer.selected_nr; i++) {
      +		struct bitmapped_commit *selected = &writer.selected[table[i]];
     -+		struct object_id *oid = &selected->commit->object.oid;
     ++		uint32_t xor_offset = selected->xor_offset;
      +
     -+		hashwrite(f, oid->hash, the_hash_algo->rawsz);
     ++		hashwrite_be32(f, commit_positions[table[i]]);
     ++		hashwrite_be64(f, offsets[table[i]]);
     ++		hashwrite_be32(f, xor_offset ?
     ++				table_inv[table[i] - xor_offset]: 0xffffffff);
      +	}
     -+	for (i = 0; i < writer.selected_nr; i++) {
     -+		struct bitmapped_commit *selected = &writer.selected[table[i]];
     -+
     -+		hashwrite_be32(f, offsets[table[i]]);
     -+		hashwrite_be32(f, selected->xor_offset
     -+			       ? table_inv[table[i] - selected->xor_offset]
     -+			       : 0xffffffff);
     -+	}
     -+
     -+	hashwrite_be32(f, flags);
      +
      +	free(table);
      +	free(table_inv);
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       {
       	static uint16_t default_version = 1;
       	static uint16_t flags = BITMAP_OPT_FULL_DAG;
     -+	off_t *offsets = NULL;
     ++	uint64_t *offsets = NULL;
     ++	uint32_t *commit_positions = NULL;
       	struct strbuf tmp_file = STRBUF_INIT;
       	struct hashfile *f;
       
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       	dump_bitmap(f, writer.tags);
      -	write_selected_commits_v1(f, index, index_nr);
       
     -+	if (options & BITMAP_OPT_LOOKUP_TABLE)
     ++	if (options & BITMAP_OPT_LOOKUP_TABLE) {
      +		CALLOC_ARRAY(offsets, index_nr);
     ++		CALLOC_ARRAY(commit_positions, index_nr);
     ++	}
      +
     -+	write_selected_commits_v1(f, index, index_nr, offsets);
     ++	write_selected_commits_v1(f, index, index_nr, offsets, commit_positions);
      +
      +	if (options & BITMAP_OPT_LOOKUP_TABLE)
     -+		write_lookup_table(f, offsets);
     ++		write_lookup_table(f, offsets, commit_positions);
       	if (options & BITMAP_OPT_HASH_CACHE)
       		write_hash_cache(f, index, index_nr);
       
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       
       	strbuf_release(&tmp_file);
      +	free(offsets);
     ++	free(commit_positions);
       }
     +
     + ## pack-bitmap.h ##
     +@@ pack-bitmap.h: struct bitmap_disk_header {
     + #define NEEDS_BITMAP (1u<<22)
     + 
     + enum pack_bitmap_opts {
     +-	BITMAP_OPT_FULL_DAG = 1,
     +-	BITMAP_OPT_HASH_CACHE = 4,
     ++	BITMAP_OPT_FULL_DAG = 0x1,
     ++	BITMAP_OPT_HASH_CACHE = 0x4,
     ++	BITMAP_OPT_LOOKUP_TABLE = 0x10,
     + };
     + 
     + enum pack_bitmap_flags {

4: 661c1137e1c ! 3: 7786dc879f0 builtin/pack-objects.c: learn pack.writeBitmapLookupTable
@@
## Metadata ##
-Author: Taylor Blau <[email protected]>
+Author: Abhradeep Chakraborty <[email protected]>

## Commit message ##
- builtin/pack-objects.c: learn pack.writeBitmapLookupTable
+ pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

Teach git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'.
+ Default is true.

- Signed-off-by: Taylor Blau <[email protected]>
+ Also add test to verify writting of lookup table.
+
+ Co-Authored-by: Taylor Blau <[email protected]>

+ Mentored-by: Taylor Blau <[email protected]>
+ Co-Mentored-by: Kaartic Sivaraam <[email protected]>

## Documentation/config/pack.txt ##
@@ Documentation/config/pack.txt: When writing a multi-pack reachability bitmap, no new namehashes are
@@ Documentation/config/pack.txt: When writing a multi-pack reachability bitmap, no
+ bitmap index (if one is written). This table is used to defer
+ loading individual bitmaps as late as possible. This can be
+ beneficial in repositories which have relatively large bitmap
-+ indexes. Defaults to false.
++ indexes. Defaults to true.
+
pack.writeReverseIndex::
When true, git will write a corresponding .rev file (see:
link:../technical/pack-format.html[Documentation/technical/pack-format.txt])

+ ## builtin/multi-pack-index.c ##
+@@ builtin/multi-pack-index.c: static int git_multi_pack_index_write_config(const char *var, const char *value,
+ opts.flags &= ~MIDX_WRITE_BITMAP_HASH_CACHE;
+ }
+
++ if (!strcmp(var, "pack.writebitmaplookuptable")) {
++ if (git_config_bool(var, value))
++ opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
++ else
++ opts.flags &= ~MIDX_WRITE_BITMAP_LOOKUP_TABLE;
++ }
++
+ /*
+ * We should never make a fall-back call to 'git_default_config', since
+ * this was already called in 'cmd_multi_pack_index()'.
+@@ builtin/multi-pack-index.c: static int cmd_multi_pack_index_write(int argc, const char **argv)
+ };
+
+ opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE;
++ opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+
+ git_config(git_multi_pack_index_write_config, NULL);
+
+
## builtin/pack-objects.c ##
+@@ builtin/pack-objects.c: static enum {
+ WRITE_BITMAP_QUIET,
+ WRITE_BITMAP_TRUE,
+ } write_bitmap_index;
+-static uint16_t write_bitmap_options = BITMAP_OPT_HASH_CACHE;
++static uint16_t write_bitmap_options = BITMAP_OPT_HASH_CACHE | BITMAP_OPT_LOOKUP_TABLE;
+
+ static int exclude_promisor_objects;
+
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v, void *cb)
else
write_bitmap_options &= ~BITMAP_OPT_HASH_CACHE;
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
if (!strcmp(k, "pack.usebitmaps")) {
use_bitmap_index_default = git_config_bool(k, v);
return 0;
+
+ ## midx.c ##
+@@ midx.c: static int write_midx_bitmap(char *midx_name, unsigned char *midx_hash,
+ if (flags & MIDX_WRITE_BITMAP_HASH_CACHE)
+ options |= BITMAP_OPT_HASH_CACHE;
+
++ if (flags & MIDX_WRITE_BITMAP_LOOKUP_TABLE)
++ options |= BITMAP_OPT_LOOKUP_TABLE;
++
+ prepare_midx_packing_data(&pdata, ctx);
+
+ commits = find_commits_for_midx_bitmap(&commits_nr, refs_snapshot, ctx);
+
+ ## midx.h ##
+@@ midx.h: struct multi_pack_index {
+ #define MIDX_WRITE_REV_INDEX (1 << 1)
+ #define MIDX_WRITE_BITMAP (1 << 2)
+ #define MIDX_WRITE_BITMAP_HASH_CACHE (1 << 3)
++#define MIDX_WRITE_BITMAP_LOOKUP_TABLE (1 << 4)
+
+ const unsigned char *get_midx_checksum(struct multi_pack_index *m);
+ void get_midx_filename(struct strbuf *out, const char *object_dir);
+
+ ## pack-bitmap-write.c ##
+@@ pack-bitmap-write.c: static void write_lookup_table(struct hashfile *f,
+ for (i = 0; i < writer.selected_nr; i++)
+ table_inv[table[i]] = i;
+
++ trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
+ for (i = 0; i < writer.selected_nr; i++) {
+ struct bitmapped_commit *selected = &writer.selected[table[i]];
+ uint32_t xor_offset = selected->xor_offset;
+@@ pack-bitmap-write.c: static void write_lookup_table(struct hashfile *f,
+
+ free(table);
+ free(table_inv);
++ trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
+ }
+
+ static void write_hash_cache(struct hashfile *f,
+
+ ## t/t5310-pack-bitmaps.sh ##
+@@ t/t5310-pack-bitmaps.sh: test_expect_success 'full repack creates bitmaps' '
+ ls .git/objects/pack/ | grep bitmap >output &&
+ test_line_count = 1 output &&
+ grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace &&
+- grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace
++ grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace &&
++ grep "\"label\":\"writing_lookup_table\"" trace
+ '
+
+ basic_bitmap_tests
+
+ ## t/t5326-multi-pack-bitmaps.sh ##
+@@ t/t5326-multi-pack-bitmaps.sh: test_expect_success 'graceful fallback when missing reverse index' '
+ )
+ '
+
++test_expect_success 'multi-pack-index write writes lookup table if enabled' '
++ rm -fr repo &&
++ git init repo &&
++ test_when_finished "rm -fr repo" &&
++ (
++ cd repo &&
++ test_commit base &&
++ git repack -ad &&
++ GIT_TRACE2_EVENT="$(pwd)/trace" \
++ git multi-pack-index write --bitmap &&
++ grep "\"label\":\"writing_lookup_table\"" trace
++ )
++'
+ test_done
2: d139a4c48aa ! 4: 4fbfcff8a20 pack-bitmap: prepare to read lookup table extension

     @@ Metadata
       ## Commit message ##
          pack-bitmap: prepare to read lookup table extension
      
     -    Bitmap lookup table extension can let git to parse only the necessary
     -    bitmaps without loading the previous bitmaps one by one.
     +    Earlier change teaches Git to write bitmap lookup table. But Git
     +    does not know how to parse them.
      
     -    Teach git to read and use the bitmap lookup table extension.
     +    Teach Git to parse the existing bitmap lookup table. The older
     +    versions of git are not affected by it. Those versions ignore the
     +    lookup table.
      
     -    Co-Authored-by: Taylor Blau <[email protected]>
     -    Mentored-by: Taylor Blau <[email protected]>
     -    Co-Mentored-by: Kaartic Sivaraam <[email protected]>
          Signed-off-by: Abhradeep Chakraborty <[email protected]>
     +    Mentored-by: Taylor Blau <[email protected]>
     +    Co-Mentored-by: Kaartic Sivaraam <[email protected]>
      
       ## pack-bitmap.c ##
     -@@
     - #include "list-objects-filter-options.h"
     - #include "midx.h"
     - #include "config.h"
     -+#include "hash-lookup.h"
     - 
     - /*
     -  * An entry on the bitmap index, representing the bitmap for a given
      @@ pack-bitmap.c: struct bitmap_index {
       	/* The checksum of the packfile or MIDX; points into map. */
       	const unsigned char *checksum;
       
      +	/*
     -+	 * If not NULL, these point into the various commit table sections
     ++	 * If not NULL, this point into the commit table extension
      +	 * (within map).
      +	 */
      +	unsigned char *table_lookup;
     -+	unsigned char *table_offsets;
      +
       	/*
       	 * Extended index.
     @@ pack-bitmap.c: static int load_bitmap_header(struct bitmap_index *index)
       		}
      +
      +		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
     -+		    git_env_bool("GIT_READ_COMMIT_TABLE", 1)) {
     -+			uint32_t entry_count = ntohl(header->entry_count);
     -+			uint32_t table_size =
     -+				(entry_count * the_hash_algo->rawsz) /* oids */ +
     -+				(entry_count * sizeof(uint32_t)) /* offsets */ +
     -+				(entry_count * sizeof(uint32_t)) /* xor offsets */ +
     -+				(sizeof(uint32_t)) /* flags */;
     ++			git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {
     ++			size_t table_size = 0;
     ++			size_t triplet_sz = st_add3(sizeof(uint32_t),    /* commit position */
     ++							sizeof(uint64_t),    /* offset */
     ++							sizeof(uint32_t));    /* xor offset */
      +
     ++			table_size = st_add(table_size,
     ++					st_mult(ntohl(header->entry_count),
     ++						triplet_sz));
      +			if (table_size > index_end - index->map - header_size)
     -+				return error("corrupted bitmap index file (too short to fit commit table)");
     -+
     ++				return error("corrupted bitmap index file (too short to fit lookup table)");
      +			index->table_lookup = (void *)(index_end - table_size);
     -+			index->table_offsets = index->table_lookup + the_hash_algo->rawsz * entry_count;
     -+
      +			index_end -= table_size;
      +		}
       	}
       
       	index->entry_count = ntohl(header->entry_count);
     +@@ pack-bitmap.c: static struct stored_bitmap *store_bitmap(struct bitmap_index *index,
     + 
     + 	hash_pos = kh_put_oid_map(index->bitmaps, stored->oid, &ret);
     + 
     +-	/* a 0 return code means the insertion succeeded with no changes,
     +-	 * because the SHA1 already existed on the map. this is bad, there
     +-	 * shouldn't be duplicated commits in the index */
     ++	/* A 0 return code means the insertion succeeded with no changes,
     ++	 * because the SHA1 already existed on the map. If lookup table
     ++	 * is NULL, this is bad, there shouldn't be duplicated commits
     ++	 * in the index.
     ++	 *
     ++	 * If table_lookup exists, that means the desired bitmap is already
     ++	 * loaded. Either this bitmap has been stored directly or another
     ++	 * bitmap has a direct or indirect xor relation with it. */
     + 	if (ret == 0) {
     +-		error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
     +-		return NULL;
     ++		if (!index->table_lookup) {
     ++			error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
     ++			return NULL;
     ++		}
     ++		return kh_value(index->bitmaps, hash_pos);
     + 	}
     + 
     + 	kh_value(index->bitmaps, hash_pos) = stored;
      @@ pack-bitmap.c: static int load_bitmap(struct bitmap_index *bitmap_git)
       		!(bitmap_git->tags = read_bitmap_1(bitmap_git)))
       		goto failed;
     @@ pack-bitmap.c: struct include_data {
       	struct bitmap *seen;
       };
       
     --struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
     --				      struct commit *commit)
     -+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
     -+						      struct commit *commit,
     -+						      uint32_t *pos_hint);
     -+
     -+static inline const unsigned char *bitmap_oid_pos(struct bitmap_index *bitmap_git,
     -+						  uint32_t pos)
     ++static inline const void *bitmap_get_triplet(struct bitmap_index *bitmap_git, uint32_t xor_pos)
      +{
     -+	return bitmap_git->table_lookup + (pos * the_hash_algo->rawsz);
     ++	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));
     ++	const void *p = bitmap_git->table_lookup + st_mult(xor_pos, triplet_sz);
     ++	return p;
      +}
      +
     -+static inline const void *bitmap_offset_pos(struct bitmap_index *bitmap_git,
     -+					    uint32_t pos)
     ++static uint64_t triplet_get_offset(const void *triplet)
      +{
     -+	return bitmap_git->table_offsets + (pos * 2 * sizeof(uint32_t));
     ++	const void *p = (unsigned char*) triplet + sizeof(uint32_t);
     ++	return get_be64(p);
      +}
      +
     -+static inline const void *xor_position_pos(struct bitmap_index *bitmap_git,
     -+					   uint32_t pos)
     ++static uint32_t triplet_get_xor_pos(const void *triplet)
      +{
     -+	return (unsigned char*) bitmap_offset_pos(bitmap_git, pos) + sizeof(uint32_t);
     ++	const void *p = (unsigned char*) triplet + st_add(sizeof(uint32_t), sizeof(uint64_t));
     ++	return get_be32(p);
      +}
      +
     -+static int bitmap_lookup_cmp(const void *_va, const void *_vb)
     ++static int triplet_cmp(const void *va, const void *vb)
      +{
     -+	return hashcmp(_va, _vb);
     ++	int result = 0;
     ++	uint32_t *a = (uint32_t *) va;
     ++	uint32_t b = get_be32(vb);
     ++	if (*a > b)
     ++		result = 1;
     ++	else if (*a < b)
     ++		result = -1;
     ++	else
     ++		result = 0;
     ++
     ++	return result;
      +}
      +
     -+static int bitmap_table_lookup(struct bitmap_index *bitmap_git,
     -+			       struct object_id *oid,
     -+			       uint32_t *commit_pos)
     ++static uint32_t bsearch_pos(struct bitmap_index *bitmap_git, struct object_id *oid,
     ++						uint32_t *result)
      +{
     -+	unsigned char *found = bsearch(oid->hash, bitmap_git->table_lookup,
     -+				       bitmap_git->entry_count,
     -+				       the_hash_algo->rawsz, bitmap_lookup_cmp);
     -+	if (found)
     -+		*commit_pos = (found - bitmap_git->table_lookup) / the_hash_algo->rawsz;
     -+	return !!found;
     ++	int found;
     ++
     ++	if (bitmap_git->midx)
     ++		found = bsearch_midx(oid, bitmap_git->midx, result);
     ++	else
     ++		found = bsearch_pack(oid, bitmap_git->pack, result);
     ++
     ++	return found;
      +}
      +
      +static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
     -+						    struct object_id *oid,
     -+						    uint32_t commit_pos)
     ++					  struct commit *commit)
      +{
     -+	uint32_t xor_pos;
     -+	off_t bitmap_ofs;
     -+
     ++	uint32_t commit_pos, xor_pos;
     ++	uint64_t offset;
      +	int flags;
     ++	const void *triplet = NULL;
     ++	struct object_id *oid = &commit->object.oid;
      +	struct ewah_bitmap *bitmap;
     -+	struct stored_bitmap *xor_bitmap;
     ++	struct stored_bitmap *xor_bitmap = NULL;
     ++	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));
      +
     -+	bitmap_ofs = get_be32(bitmap_offset_pos(bitmap_git, commit_pos));
     -+	xor_pos = get_be32(xor_position_pos(bitmap_git, commit_pos));
     ++	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
      +
     -+	/*
     -+	 * Lazily load the xor'd bitmap if required (and we haven't done so
     -+	 * already). Make sure to pass the xor'd bitmap's position along as a
     -+	 * hint to avoid an unnecessary binary search in
     -+	 * stored_bitmap_for_commit().
     -+	 */
     -+	if (xor_pos == 0xffffffff) {
     -+		xor_bitmap = NULL;
     -+	} else {
     -+		struct commit *xor_commit;
     ++	if (!found)
     ++		return NULL;
     ++
     ++	triplet = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
     ++						triplet_sz, triplet_cmp);
     ++	if (!triplet)
     ++		return NULL;
     ++
     ++	offset = triplet_get_offset(triplet);
     ++	xor_pos = triplet_get_xor_pos(triplet);
     ++
     ++	if (xor_pos != 0xffffffff) {
     ++		int xor_flags;
     ++		uint64_t offset_xor;
     ++		uint32_t *xor_positions;
      +		struct object_id xor_oid;
     ++		size_t size = 0;
      +
     -+		oidread(&xor_oid, bitmap_oid_pos(bitmap_git, xor_pos));
     ++		ALLOC_ARRAY(xor_positions, bitmap_git->entry_count);
     ++		while (xor_pos != 0xffffffff) {
     ++			xor_positions[size++] = xor_pos;
     ++			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
     ++			xor_pos = triplet_get_xor_pos(triplet);
     ++		}
      +
     -+		xor_commit = lookup_commit(the_repository, &xor_oid);
     -+		if (!xor_commit)
     -+			return NULL;
     ++		while (size){
     ++			xor_pos = xor_positions[size - 1];
     ++			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
     ++			commit_pos = get_be32(triplet);
     ++			offset_xor = triplet_get_offset(triplet);
     ++
     ++			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, commit_pos) < 0) {
     ++				free(xor_positions);
     ++				return NULL;
     ++			}
     ++
     ++			bitmap_git->map_pos = offset_xor + sizeof(uint32_t) + sizeof(uint8_t);
     ++			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
     ++			bitmap = read_bitmap_1(bitmap_git);
     ++
     ++			if (!bitmap){
     ++				free(xor_positions);
     ++				return NULL;
     ++			}
     ++
     ++			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_oid, xor_bitmap, xor_flags);
     ++			size--;
     ++		}
      +
     -+		xor_bitmap = stored_bitmap_for_commit(bitmap_git, xor_commit,
     -+						      &xor_pos);
     ++		free(xor_positions);
      +	}
      +
     -+	/*
     -+	 * Don't bother reading the commit's index position or its xor
     -+	 * offset:
     -+	 *
     -+	 *   - The commit's index position is irrelevant to us, since
     -+	 *     load_bitmap_entries_v1 only uses it to learn the object
     -+	 *     id which is used to compute the hashmap's key. We already
     -+	 *     have an object id, so no need to look it up again.
     -+	 *
     -+	 *   - The xor_offset is unusable for us, since it specifies how
     -+	 *     many entries previous to ours we should look at. This
     -+	 *     makes sense when reading the bitmaps sequentially (as in
     -+	 *     load_bitmap_entries_v1()), since we can keep track of
     -+	 *     each bitmap as we read them.
     -+	 *
     -+	 *     But it can't work for us, since the bitmap's don't have a
     -+	 *     fixed size. So we learn the position of the xor'd bitmap
     -+	 *     from the commit table (and resolve it to a bitmap in the
     -+	 *     above if-statement).
     -+	 *
     -+	 * Instead, we can skip ahead and immediately read the flags and
     -+	 * ewah bitmap.
     -+	 */
     -+	bitmap_git->map_pos = bitmap_ofs + sizeof(uint32_t) + sizeof(uint8_t);
     ++	bitmap_git->map_pos = offset + sizeof(uint32_t) + sizeof(uint8_t);
      +	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
      +	bitmap = read_bitmap_1(bitmap_git);
     ++
      +	if (!bitmap)
      +		return NULL;
      +
      +	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);
      +}
      +
     -+static struct stored_bitmap *stored_bitmap_for_commit(struct bitmap_index *bitmap_git,
     -+						      struct commit *commit,
     -+						      uint32_t *pos_hint)
     + struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
     + 				      struct commit *commit)
       {
       	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
       					   commit->object.oid);
      -	if (hash_pos >= kh_end(bitmap_git->bitmaps))
     +-		return NULL;
      +	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
     -+		uint32_t commit_pos;
     ++		struct stored_bitmap *bitmap = NULL;
      +		if (!bitmap_git->table_lookup)
      +			return NULL;
      +
     -+		/* NEEDSWORK: cache misses aren't recorded. */
     -+		if (pos_hint)
     -+			commit_pos = *pos_hint;
     -+		else if (!bitmap_table_lookup(bitmap_git,
     -+					      &commit->object.oid,
     -+					      &commit_pos))
     ++		/* NEEDSWORK: cache misses aren't recorded */
     ++		bitmap = lazy_bitmap_for_commit(bitmap_git, commit);
     ++		if(!bitmap)
      +			return NULL;
     -+		return lazy_bitmap_for_commit(bitmap_git, &commit->object.oid,
     -+					      commit_pos);
     ++		return lookup_stored_bitmap(bitmap);
      +	}
     -+	return kh_value(bitmap_git->bitmaps, hash_pos);
     -+}
     -+
     -+struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
     -+				      struct commit *commit)
     -+{
     -+	struct stored_bitmap *sb = stored_bitmap_for_commit(bitmap_git, commit,
     -+							    NULL);
     -+	if (!sb)
     - 		return NULL;
     --	return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos));
     -+	return lookup_stored_bitmap(sb);
     + 	return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos));
       }
       
     - static inline int bitmap_position_extended(struct bitmap_index *bitmap_git,
      @@ pack-bitmap.c: void test_bitmap_walk(struct rev_info *revs)
       	if (revs->pending.nr != 1)
       		die("you must specify exactly one commit to test");
       
      -	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
     --		bitmap_git->version, bitmap_git->entry_count);
     ++	fprintf(stderr, "Bitmap v%d test (%d entries)\n",
     + 		bitmap_git->version, bitmap_git->entry_count);
     + 
      +	if (!bitmap_git->table_lookup)
      +		fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
      +			bitmap_git->version, bitmap_git->entry_count);
     - 
     ++
       	root = revs->pending.objects[0].item;
       	bm = bitmap_for_commit(bitmap_git, (struct commit *)root);
     + 
     +@@ pack-bitmap.c: void test_bitmap_walk(struct rev_info *revs)
     + 
     + int test_bitmap_commits(struct repository *r)
     + {
     +-	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
     ++	struct bitmap_index *bitmap_git = NULL;
     + 	struct object_id oid;
     + 	MAYBE_UNUSED void *value;
     + 
     ++	/* As this function is only used to print bitmap selected
     ++	 * commits, we don't have to read the commit table.
     ++	 */
     ++	setenv("GIT_TEST_READ_COMMIT_TABLE", "0", 1);
     ++
     ++	bitmap_git = prepare_bitmap_git(r);
     + 	if (!bitmap_git)
     + 		die("failed to load bitmap indexes");
     + 
     +@@ pack-bitmap.c: int test_bitmap_commits(struct repository *r)
     + 		printf("%s\n", oid_to_hex(&oid));
     + 	});
     + 
     ++	setenv("GIT_TEST_READ_COMMIT_TABLE", "1", 1);
     + 	free_bitmap_index(bitmap_git);
     + 
     + 	return 0;
      
     - ## pack-bitmap.h ##
     -@@ pack-bitmap.h: struct bitmap_disk_header {
     - enum pack_bitmap_opts {
     - 	BITMAP_OPT_FULL_DAG = 1,
     - 	BITMAP_OPT_HASH_CACHE = 4,
     -+	BITMAP_OPT_LOOKUP_TABLE = 16,
     - };
     + ## t/t5310-pack-bitmaps.sh ##
     +@@ t/t5310-pack-bitmaps.sh: test_expect_success 'full repack creates bitmaps' '
     + 	grep "\"label\":\"writing_lookup_table\"" trace
     + '
     + 
     ++test_expect_success 'using lookup table loads only necessary bitmaps' '
     ++	git rev-list --test-bitmap HEAD 2>out &&
     ++	! grep "Bitmap v1 test (106 entries loaded)" out &&
     ++	grep "Found bitmap for" out
     ++'
     ++
     + basic_bitmap_tests
       
     - enum pack_bitmap_flags {
     + test_expect_success 'incremental repack fails when bitmaps are requested' '
     +@@ t/t5310-pack-bitmaps.sh: test_expect_success 'pack reuse respects --incremental' '
     + 
     + test_expect_success 'truncated bitmap fails gracefully (ewah)' '
     + 	test_config pack.writebitmaphashcache false &&
     ++	test_config pack.writebitmaplookuptable false &&
     + 	git repack -ad &&
     + 	git rev-list --use-bitmap-index --count --all >expect &&
     + 	bitmap=$(ls .git/objects/pack/*.bitmap) &&
     +
     + ## t/t5326-multi-pack-bitmaps.sh ##
     +@@ t/t5326-multi-pack-bitmaps.sh: test_expect_success 'multi-pack-index write writes lookup table if enabled' '
     + 		grep "\"label\":\"writing_lookup_table\"" trace
     + 	)
     + '
     ++
     + test_done
 5:  a404779a30f < -:  ----------- bitmap-commit-table: add tests for the bitmap lookup table
 6:  f5f725a3fe2 ! 5:  96c0041688f bitmap-lookup-table: add performance tests

@@ Metadata
Author: Abhradeep Chakraborty <[email protected]>

## Commit message ##
- bitmap-lookup-table: add performance tests
+ bitmap-lookup-table: add performance tests for lookup table

- Add performance tests for bitmap lookup table extension.
+ Add performance tests to verify the performance of lookup table.
+
+ Lookup table makes Git run faster in most of the cases. Below is the
+ result of t/perf/p5310-pack-bitmaps.sh.perf/p5326-multi-pack-bitmaps.sh
+ gives similar result. The repository used in the test is linux kernel.
+
+ Test this tree
+ --------------------------------------------------------------------------
+ 5310.4: repack to disk (lookup=false) 295.94(250.45+15.24)
+ 5310.5: simulated clone 12.52(5.07+1.40)
+ 5310.6: simulated fetch 1.89(2.94+0.24)
+ 5310.7: pack to file (bitmap) 41.39(20.33+7.20)
+ 5310.8: rev-list (commits) 0.98(0.59+0.12)
+ 5310.9: rev-list (objects) 3.40(3.27+0.10)
+ 5310.10: rev-list with tag negated via --not 0.07(0.02+0.04)
+ --all (objects)
+ 5310.11: rev-list with negative tag (objects) 0.23(0.16+0.06)
+ 5310.12: rev-list count with blob:none 0.26(0.18+0.07)
+ 5310.13: rev-list count with blob:limit=1k 6.45(5.94+0.37)
+ 5310.14: rev-list count with tree:0 0.26(0.18+0.07)
+ 5310.15: simulated partial clone 4.99(3.19+0.45)
+ 5310.19: repack to disk (lookup=true) 269.67(174.70+21.33)
+ 5310.20: simulated clone 11.03(5.07+1.11)
+ 5310.21: simulated fetch 0.79(0.79+0.17)
+ 5310.22: pack to file (bitmap) 43.03(20.28+7.43)
+ 5310.23: rev-list (commits) 0.86(0.54+0.09)
+ 5310.24: rev-list (objects) 3.35(3.26+0.07)
+ 5310.25: rev-list with tag negated via --not 0.05(0.00+0.03)
+ --all (objects)
+ 5310.26: rev-list with negative tag (objects) 0.22(0.16+0.05)
+ 5310.27: rev-list count with blob:none 0.22(0.16+0.05)
+ 5310.28: rev-list count with blob:limit=1k 6.45(5.87+0.31)
+ 5310.29: rev-list count with tree:0 0.22(0.16+0.05)
+ 5310.30: simulated partial clone 5.17(3.12+0.48)
+
+ Test 4-15 are tested without using lookup table. Same tests are
+ repeated in 16-30 (using lookup table).

- Mentored-by: Taylor Blau <[email protected]>
- Co-mentored-by: Kaartic Sivaraam <[email protected]>

+ Mentored-by: Taylor Blau <[email protected]>
+ Co-Mentored-by: Kaartic Sivaraam <[email protected]>

## t/perf/p5310-pack-bitmaps.sh ##
-@@ t/perf/p5310-pack-bitmaps.sh: test_perf_large_repo
- # since we want to be able to compare bitmap-aware
- # git versus non-bitmap git
- #
--# We intentionally use the deprecated pack.writebitmaps
-+# We intentionally use the deprecated pack.writeBitmaps
- # config so that we can test against older versions of git.
- test_expect_success 'setup bitmap config' '
-- git config pack.writebitmaps true
-+ git config pack.writeBitmaps true &&
-+ git config pack.writeReverseIndex true
+@@ t/perf/p5310-pack-bitmaps.sh: test_expect_success 'setup bitmap config' '
+ git config pack.writebitmaps true
'

- # we need to create the tag up front such that it is covered by the repack and -@@ t/perf/p5310-pack-bitmaps.sh: test_perf 'repack to disk' '
-
- test_full_bitmap
-
+-# we need to create the tag up front such that it is covered by the repack and +-# thus by generated bitmaps.
+-test_expect_success 'create tags' '
+- git tag --message="tag pointing to HEAD" perf-tag HEAD
+-'
+-
+-test_perf 'repack to disk' '
+- git repack -ad
+-'
+-
+-test_full_bitmap
+-
-test_expect_success 'create partial bitmap state' '
- # pick a commit to represent the repo tip in the past
- cutoff=$(git rev-list HEAD~100 -1) &&
@@ t/perf/p5310-pack-bitmaps.sh: test_perf 'repack to disk' '
- # and now restore our original tip, as if the pushes
- # had happened
- git update-ref HEAD $orig_tip
-+test_perf 'use lookup table' '
-+ git config pack.writeBitmapLookupTable true
- '
-
+-'
+-
-test_partial_bitmap
-+test_perf 'repack to disk (lookup table)' '
-+ git repack -adb
-+'
++test_bitmap () {
++ local enabled="$1"
+
-+test_full_bitmap
++ # we need to create the tag up front such that it is covered by the repack and
++ # thus by generated bitmaps.
++ test_expect_success 'create tags' '
++ git tag --message="tag pointing to HEAD" perf-tag HEAD
++ '
+
-+for i in false true
-+do
-+ $i && lookup=" (lookup table)"
-+ test_expect_success "create partial bitmap state$lookup" '
-+ git config pack.writeBitmapLookupTable '"$i"' &&
++ test_expect_success "use lookup table: $enabled" '
++ git config pack.writeBitmapLookupTable '"$enabled"'
++ '
++
++ test_perf "repack to disk (lookup=$enabled)" '
++ git repack -ad
++ '
++
++ test_full_bitmap
++
++ test_expect_success "create partial bitmap state (lookup=$enabled)" '
+ # pick a commit to represent the repo tip in the past
+ cutoff=$(git rev-list HEAD~100 -1) &&
+ orig_tip=$(git rev-parse HEAD) &&
@@ t/perf/p5310-pack-bitmaps.sh: test_perf 'repack to disk' '
+ # and now restore our original tip, as if the pushes
+ # had happened
+ git update-ref HEAD $orig_tip
-+ '
++ '
++}
+
-+ test_partial_bitmap
-+done
++test_bitmap false
++test_bitmap true

test_done

## t/perf/p5326-multi-pack-bitmaps.sh ##
-@@ t/perf/p5326-multi-pack-bitmaps.sh: test_expect_success 'drop pack bitmap' ' +@@ t/perf/p5326-multi-pack-bitmaps.sh: test_description='Tests performance using midx bitmaps'

- test_full_bitmap
+ test_perf_large_repo

+-# we need to create the tag up front such that it is covered by the repack and +-# thus by generated bitmaps.
+-test_expect_success 'create tags' '
+- git tag --message="tag pointing to HEAD" perf-tag HEAD
+-'
+-
+-test_expect_success 'start with bitmapped pack' '
+- git repack -adb
+-'
+-
+-test_perf 'setup multi-pack index' '
+- git multi-pack-index write --bitmap
+-'
+-
+-test_expect_success 'drop pack bitmap' '
+- rm -f .git/objects/pack/pack-*.bitmap
+-'
+-
+-test_full_bitmap
+-
-test_expect_success 'create partial bitmap state' '
- # pick a commit to represent the repo tip in the past
- cutoff=$(git rev-list HEAD~100 -1) &&
@@ t/perf/p5326-multi-pack-bitmaps.sh: test_expect_success 'drop pack bitmap' '
- # and now restore our original tip, as if the pushes
- # had happened
- git update-ref HEAD $orig_tip
-+test_expect_success 'use lookup table' '
-+ git config pack.writeBitmapLookupTable true
- '
-
+-'
+-
-test_partial_bitmap
-+test_perf 'setup multi-pack-index (lookup table)' '
-+ git multi-pack-index write --bitmap
-+'
++test_bitmap () {
++ local enabled="$1"
++
++ # we need to create the tag up front such that it is covered by the repack and
++ # thus by generated bitmaps.
++ test_expect_success 'create tags' '
++ git tag --message="tag pointing to HEAD" perf-tag HEAD
++ '
+
-+test_full_bitmap
++ test_expect_success "use lookup table: $enabled" '
++ git config pack.writeBitmapLookupTable '"$enabled"'
++ '
++
++ test_expect_success "start with bitmapped pack (lookup=$enabled)" '
++ git repack -adb
++ '
++
++ test_perf "setup multi-pack index (lookup=$enabled)" '
++ git multi-pack-index write --bitmap
++ '
+
-+for i in false true
-+do
-+ $i && lookup=" (lookup table)"
-+ test_expect_success "create partial bitmap state$lookup" '
-+ git config pack.writeBitmapLookupTable '"$i"' &&
++ test_expect_success "drop pack bitmap (lookup=$enabled)" '
++ rm -f .git/objects/pack/pack-*.bitmap
++ '
++
++ test_full_bitmap
++
++ test_expect_success "create partial bitmap state (lookup=$enabled)" '
+ # pick a commit to represent the repo tip in the past
+ cutoff=$(git rev-list HEAD~100 -1) &&
+ orig_tip=$(git rev-parse HEAD) &&
@@ t/perf/p5326-multi-pack-bitmaps.sh: test_expect_success 'drop pack bitmap' '
+ # and now restore our original tip, as if the pushes
+ # had happened
+ git update-ref HEAD $orig_tip
-+ '
++ '
++}
+
-+ test_partial_bitmap
-+done
++test_bitmap false
++test_bitmap true

test_done
-: ----------- > 6: fe556b58814 p5310-pack-bitmaps.sh: enable pack.writeReverseIndex for testing

Signature · 1 line
gitgitgadget
#63 lore raw

PATCH v2 5/6 bitmap-lookup-table: add performance tests for lookup table

From: Abhradeep Chakraborty <[email protected]>

Add performance tests to verify the performance of lookup table.

Lookup table makes Git run faster in most of the cases. Below is the result of t/perf/p5310-pack-bitmaps.sh.perf/p5326-multi-pack-bitmaps.sh gives similar result. The repository used in the test is linux kernel.

Test this tree
--------------------------------------------------------------------------

5310.4: repack to disk (lookup=false)                   295.94(250.45+15.24)
5310.5: simulated clone                                 12.52(5.07+1.40)
5310.6: simulated fetch                                 1.89(2.94+0.24)
5310.7: pack to file (bitmap)                           41.39(20.33+7.20)
5310.8: rev-list (commits)                              0.98(0.59+0.12)
5310.9: rev-list (objects)                              3.40(3.27+0.10)
5310.10: rev-list with tag negated via --not		0.07(0.02+0.04)
         --all (objects)
5310.11: rev-list with negative tag (objects)           0.23(0.16+0.06)
5310.12: rev-list count with blob:none                  0.26(0.18+0.07)
5310.13: rev-list count with blob:limit=1k              6.45(5.94+0.37)
5310.14: rev-list count with tree:0                     0.26(0.18+0.07)
5310.15: simulated partial clone                        4.99(3.19+0.45)
5310.19: repack to disk (lookup=true)                   269.67(174.70+21.33)
5310.20: simulated clone                                11.03(5.07+1.11)
5310.21: simulated fetch                                0.79(0.79+0.17)
5310.22: pack to file (bitmap)                          43.03(20.28+7.43)
5310.23: rev-list (commits)                             0.86(0.54+0.09)
5310.24: rev-list (objects)                             3.35(3.26+0.07)
5310.25: rev-list with tag negated via --not		0.05(0.00+0.03)
	 --all (objects)
5310.26: rev-list with negative tag (objects)           0.22(0.16+0.05)
5310.27: rev-list count with blob:none                  0.22(0.16+0.05)
5310.28: rev-list count with blob:limit=1k              6.45(5.87+0.31)
5310.29: rev-list count with tree:0                     0.22(0.16+0.05)
5310.30: simulated partial clone                        5.17(3.12+0.48)

Test 4-15 are tested without using lookup table. Same tests are repeated in 16-30 (using lookup table).

---
 t/perf/p5310-pack-bitmaps.sh       | 77 ++++++++++++++-----------
 t/perf/p5326-multi-pack-bitmaps.sh | 93 ++++++++++++++++--------------
 2 files changed, 94 insertions(+), 76 deletions(-)
t/perf/p5310-pack-bitmaps.sh +43 -34
index 7ad4f237bc3..6ff42bdd391 100755
@@ -16,39 +16,48 @@ test_expect_success 'setup bitmap config' '
 	git config pack.writebitmaps true
 '
 
-# we need to create the tag up front such that it is covered by the repack and
-# thus by generated bitmaps.
-test_expect_success 'create tags' '
-	git tag --message="tag pointing to HEAD" perf-tag HEAD
-'
-
-test_perf 'repack to disk' '
-	git repack -ad
-'
-
-test_full_bitmap
-
-test_expect_success 'create partial bitmap state' '
-	# pick a commit to represent the repo tip in the past
-	cutoff=$(git rev-list HEAD~100 -1) &&
-	orig_tip=$(git rev-parse HEAD) &&
-
-	# now kill off all of the refs and pretend we had
-	# just the one tip
-	rm -rf .git/logs .git/refs/* .git/packed-refs &&
-	git update-ref HEAD $cutoff &&
-
-	# and then repack, which will leave us with a nice
-	# big bitmap pack of the "old" history, and all of
-	# the new history will be loose, as if it had been pushed
-	# up incrementally and exploded via unpack-objects
-	git repack -Ad &&
-
-	# and now restore our original tip, as if the pushes
-	# had happened
-	git update-ref HEAD $orig_tip
-'
-
-test_partial_bitmap
+test_bitmap () {
+    local enabled="$1"
+
+	# we need to create the tag up front such that it is covered by the repack and
+	# thus by generated bitmaps.
+	test_expect_success 'create tags' '
+		git tag --message="tag pointing to HEAD" perf-tag HEAD
+	'
+
+	test_expect_success "use lookup table: $enabled" '
+		git config pack.writeBitmapLookupTable '"$enabled"'
+	'
+
+	test_perf "repack to disk (lookup=$enabled)" '
+		git repack -ad
+	'
+
+	test_full_bitmap
+
+    test_expect_success "create partial bitmap state (lookup=$enabled)" '
+		# pick a commit to represent the repo tip in the past
+		cutoff=$(git rev-list HEAD~100 -1) &&
+		orig_tip=$(git rev-parse HEAD) &&
+
+		# now kill off all of the refs and pretend we had
+		# just the one tip
+		rm -rf .git/logs .git/refs/* .git/packed-refs &&
+		git update-ref HEAD $cutoff &&
+
+		# and then repack, which will leave us with a nice
+		# big bitmap pack of the "old" history, and all of
+		# the new history will be loose, as if it had been pushed
+		# up incrementally and exploded via unpack-objects
+		git repack -Ad &&
+
+		# and now restore our original tip, as if the pushes
+		# had happened
+		git update-ref HEAD $orig_tip
+    '
+}
+
+test_bitmap false
+test_bitmap true
 
 test_done
t/perf/p5326-multi-pack-bitmaps.sh +51 -42

98 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#67 lore raw

PATCH v2 6/6 p5310-pack-bitmaps.sh: enable pack.writeReverseIndex for testing

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Enable pack.writeReverseIndex to true to see the effect of writing the reverse index in the existing bitmap tests (with and without lookup table).

Below is the result of performance test. Output format is in seconds.

Test this tree
-------------------------------------------------------------------

5310.4: repack to disk (lookup=false)           294.92(257.60+14.29)
5310.5: simulated clone                         14.97(8.95+1.31)
5310.6: simulated fetch                         1.64(2.77+0.20)
5310.7: pack to file (bitmap)                   41.76(29.33+6.77)
5310.8: rev-list (commits)                      0.71(0.49+0.09)
5310.9: rev-list (objects)                      4.65(4.55+0.09)
5310.10: rev-list with tag negated via --not	0.08(0.02+0.05)
	 --all (objects)
5310.11: rev-list with negative tag (objects)   0.06(0.01+0.04)
5310.12: rev-list count with blob:none          0.09(0.03+0.05)
5310.13: rev-list count with blob:limit=1k      7.58(7.06+0.33)
5310.14: rev-list count with tree:0             0.09(0.03+0.06)
5310.15: simulated partial clone                8.64(8.04+0.35)
5310.19: repack to disk (lookup=true)           249.86(191.57+19.50)
5310.20: simulated clone                        13.67(8.83+1.06)
5310.21: simulated fetch                        0.50(0.63+0.13)
5310.22: pack to file (bitmap)                  41.24(28.99+6.67)
5310.23: rev-list (commits)                     0.67(0.50+0.07)
5310.24: rev-list (objects)                     4.88(4.79+0.08)
5310.25: rev-list with tag negated via --not    0.04(0.00+0.03)
	 --all (objects)
5310.26: rev-list with negative tag (objects)   0.05(0.00+0.04)
5310.27: rev-list count with blob:none          0.05(0.01+0.03)
5310.28: rev-list count with blob:limit=1k      8.02(7.16+0.34)
5310.29: rev-list count with tree:0             0.05(0.01+0.04)
5310.30: simulated partial clone                8.57(8.16+0.32)

Tests 4-15 are without the use of lookup table. The rests are repeatation of the previous tests but using lookup table.

---
 t/perf/p5310-pack-bitmaps.sh | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
t/perf/p5310-pack-bitmaps.sh +2 -1
index 6ff42bdd391..9848c5d5040 100755
@@ -13,7 +13,8 @@ test_perf_large_repo
 # We intentionally use the deprecated pack.writebitmaps
 # config so that we can test against older versions of git.
 test_expect_success 'setup bitmap config' '
-	git config pack.writebitmaps true
+	git config pack.writebitmaps true &&
+	git config pack.writeReverseIndex true
 '
 
 test_bitmap () {
Signature · 1 line
gitgitgadget
#50 lore raw

PATCH v2 4/6 pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Earlier change teaches Git to write bitmap lookup table. But Git does not know how to parse them.

Teach Git to parse the existing bitmap lookup table. The older versions of git are not affected by it. Those versions ignore the lookup table.

---
 pack-bitmap.c                 | 193 ++++++++++++++++++++++++++++++++--
 t/t5310-pack-bitmaps.sh       |   7 ++
 t/t5326-multi-pack-bitmaps.sh |   1 +
 3 files changed, 191 insertions(+), 10 deletions(-)
pack-bitmap.c +183 -10
index 36134222d7a..9e09c5824fc 100644
@@ -82,6 +82,12 @@ struct bitmap_index {
 	/* The checksum of the packfile or MIDX; points into map. */
 	const unsigned char *checksum;
 
+	/*
+	 * If not NULL, this point into the commit table extension
+	 * (within map).
+	 */
+	unsigned char *table_lookup;
+
 	/*
 	 * Extended index.
 	 *
@@ -185,6 +191,22 @@ static int load_bitmap_header(struct bitmap_index *index)
 			index->hashes = (void *)(index_end - cache_size);
 			index_end -= cache_size;
 		}
+
+		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
+			git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {
+			size_t table_size = 0;
+			size_t triplet_sz = st_add3(sizeof(uint32_t),    /* commit position */
+							sizeof(uint64_t),    /* offset */
+							sizeof(uint32_t));    /* xor offset */
+
+			table_size = st_add(table_size,
+					st_mult(ntohl(header->entry_count),
+						triplet_sz));
+			if (table_size > index_end - index->map - header_size)
+				return error("corrupted bitmap index file (too short to fit lookup table)");
+			index->table_lookup = (void *)(index_end - table_size);
+			index_end -= table_size;
+		}
 	}
 
 	index->entry_count = ntohl(header->entry_count);
@@ -211,12 +233,20 @@ static struct stored_bitmap *store_bitmap(struct bitmap_index *index,
 
 	hash_pos = kh_put_oid_map(index->bitmaps, stored->oid, &ret);
 
-	/* a 0 return code means the insertion succeeded with no changes,
-	 * because the SHA1 already existed on the map. this is bad, there
-	 * shouldn't be duplicated commits in the index */
+	/* A 0 return code means the insertion succeeded with no changes,
+	 * because the SHA1 already existed on the map. If lookup table
+	 * is NULL, this is bad, there shouldn't be duplicated commits
+	 * in the index.
+	 *
+	 * If table_lookup exists, that means the desired bitmap is already
+	 * loaded. Either this bitmap has been stored directly or another
+	 * bitmap has a direct or indirect xor relation with it. */
 	if (ret == 0) {
-		error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
-		return NULL;
+		if (!index->table_lookup) {
+			error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
+			return NULL;
+		}
+		return kh_value(index->bitmaps, hash_pos);
 	}
 
 	kh_value(index->bitmaps, hash_pos) = stored;
@@ -470,7 +500,7 @@ static int load_bitmap(struct bitmap_index *bitmap_git)
 		!(bitmap_git->tags = read_bitmap_1(bitmap_git)))
 		goto failed;
 
-	if (load_bitmap_entries_v1(bitmap_git) < 0)
+	if (!bitmap_git->table_lookup && load_bitmap_entries_v1(bitmap_git) < 0)
 		goto failed;
 
 	return 0;
@@ -557,13 +587,145 @@ struct include_data {
 	struct bitmap *seen;
 };
 
+static inline const void *bitmap_get_triplet(struct bitmap_index *bitmap_git, uint32_t xor_pos)
+{
+	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));
+	const void *p = bitmap_git->table_lookup + st_mult(xor_pos, triplet_sz);
+	return p;
+}
+
+static uint64_t triplet_get_offset(const void *triplet)
+{
+	const void *p = (unsigned char*) triplet + sizeof(uint32_t);
+	return get_be64(p);
+}
+
+static uint32_t triplet_get_xor_pos(const void *triplet)
+{
+	const void *p = (unsigned char*) triplet + st_add(sizeof(uint32_t), sizeof(uint64_t));
+	return get_be32(p);
+}
+
+static int triplet_cmp(const void *va, const void *vb)
+{
+	int result = 0;
+	uint32_t *a = (uint32_t *) va;
+	uint32_t b = get_be32(vb);
+	if (*a > b)
+		result = 1;
+	else if (*a < b)
+		result = -1;
+	else
+		result = 0;
+
+	return result;
+}
+
+static uint32_t bsearch_pos(struct bitmap_index *bitmap_git, struct object_id *oid,
+						uint32_t *result)
+{
+	int found;
+
+	if (bitmap_git->midx)
+		found = bsearch_midx(oid, bitmap_git->midx, result);
+	else
+		found = bsearch_pack(oid, bitmap_git->pack, result);
+
+	return found;
+}
+
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+					  struct commit *commit)
+{
+	uint32_t commit_pos, xor_pos;
+	uint64_t offset;
+	int flags;
+	const void *triplet = NULL;
+	struct object_id *oid = &commit->object.oid;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap = NULL;
+	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));
+
+	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
+
+	if (!found)
+		return NULL;
+
+	triplet = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+						triplet_sz, triplet_cmp);
+	if (!triplet)
+		return NULL;
+
+	offset = triplet_get_offset(triplet);
+	xor_pos = triplet_get_xor_pos(triplet);
+
+	if (xor_pos != 0xffffffff) {
+		int xor_flags;
+		uint64_t offset_xor;
+		uint32_t *xor_positions;
+		struct object_id xor_oid;
+		size_t size = 0;
+
+		ALLOC_ARRAY(xor_positions, bitmap_git->entry_count);
+		while (xor_pos != 0xffffffff) {
+			xor_positions[size++] = xor_pos;
+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
+			xor_pos = triplet_get_xor_pos(triplet);
+		}
+
+		while (size){
+			xor_pos = xor_positions[size - 1];
+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
+			commit_pos = get_be32(triplet);
+			offset_xor = triplet_get_offset(triplet);
+
+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, commit_pos) < 0) {
+				free(xor_positions);
+				return NULL;
+			}
+
+			bitmap_git->map_pos = offset_xor + sizeof(uint32_t) + sizeof(uint8_t);
+			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+			bitmap = read_bitmap_1(bitmap_git);
+
+			if (!bitmap){
+				free(xor_positions);
+				return NULL;
+			}
+
+			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_oid, xor_bitmap, xor_flags);
+			size--;
+		}
+
+		free(xor_positions);
+	}
+
+	bitmap_git->map_pos = offset + sizeof(uint32_t) + sizeof(uint8_t);
+	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+	bitmap = read_bitmap_1(bitmap_git);
+
+	if (!bitmap)
+		return NULL;
+
+	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);
+}
+
 struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
 				      struct commit *commit)
 {
 	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
 					   commit->object.oid);
-	if (hash_pos >= kh_end(bitmap_git->bitmaps))
-		return NULL;
+	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
+		struct stored_bitmap *bitmap = NULL;
+		if (!bitmap_git->table_lookup)
+			return NULL;
+
+		/* NEEDSWORK: cache misses aren't recorded */
+		bitmap = lazy_bitmap_for_commit(bitmap_git, commit);
+		if(!bitmap)
+			return NULL;
+		return lookup_stored_bitmap(bitmap);
+	}
 	return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos));
 }
 
@@ -1699,9 +1861,13 @@ void test_bitmap_walk(struct rev_info *revs)
 	if (revs->pending.nr != 1)
 		die("you must specify exactly one commit to test");
 
-	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
+	fprintf(stderr, "Bitmap v%d test (%d entries)\n",
 		bitmap_git->version, bitmap_git->entry_count);
 
+	if (!bitmap_git->table_lookup)
+		fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
+			bitmap_git->version, bitmap_git->entry_count);
+
 	root = revs->pending.objects[0].item;
 	bm = bitmap_for_commit(bitmap_git, (struct commit *)root);
 
@@ -1753,10 +1919,16 @@ void test_bitmap_walk(struct rev_info *revs)
 
 int test_bitmap_commits(struct repository *r)
 {
-	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
+	struct bitmap_index *bitmap_git = NULL;
 	struct object_id oid;
 	MAYBE_UNUSED void *value;
 
+	/* As this function is only used to print bitmap selected
+	 * commits, we don't have to read the commit table.
+	 */
+	setenv("GIT_TEST_READ_COMMIT_TABLE", "0", 1);
+
+	bitmap_git = prepare_bitmap_git(r);
 	if (!bitmap_git)
 		die("failed to load bitmap indexes");
 
@@ -1764,6 +1936,7 @@ int test_bitmap_commits(struct repository *r)
 		printf("%s\n", oid_to_hex(&oid));
 	});
 
+	setenv("GIT_TEST_READ_COMMIT_TABLE", "1", 1);
 	free_bitmap_index(bitmap_git);
 
 	return 0;
t/t5310-pack-bitmaps.sh +7 -0
index c669ed959e9..10d7691d973 100755
@@ -42,6 +42,12 @@ test_expect_success 'full repack creates bitmaps' '
 	grep "\"label\":\"writing_lookup_table\"" trace
 '
 
+test_expect_success 'using lookup table loads only necessary bitmaps' '
+	git rev-list --test-bitmap HEAD 2>out &&
+	! grep "Bitmap v1 test (106 entries loaded)" out &&
+	grep "Found bitmap for" out
+'
+
 basic_bitmap_tests
 
 test_expect_success 'incremental repack fails when bitmaps are requested' '
@@ -255,6 +261,7 @@ test_expect_success 'pack reuse respects --incremental' '
 
 test_expect_success 'truncated bitmap fails gracefully (ewah)' '
 	test_config pack.writebitmaphashcache false &&
+	test_config pack.writebitmaplookuptable false &&
 	git repack -ad &&
 	git rev-list --use-bitmap-index --count --all >expect &&
 	bitmap=$(ls .git/objects/pack/*.bitmap) &&
t/t5326-multi-pack-bitmaps.sh +1 -0
index 43be49617b8..7d36dbcf722 100755
@@ -320,4 +320,5 @@ test_expect_success 'multi-pack-index write writes lookup table if enabled' '
 		grep "\"label\":\"writing_lookup_table\"" trace
 	)
 '
+
 test_done
Signature · 1 line
gitgitgadget
#34 lore raw

Re: [PATCH v2 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 8 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx or pack, along with their offset and xor offset. This way git can load only the neccesary bitmaps without loading the previous bitmaps.

s/neccesary/necessary/

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 73

Quoted 4 lines
Documentation/technical/bitmap-format.txt +4 -0
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` <commit pos, offset, xor offset>

(Note that "commit pos" and "xor offset" here don't have underscores, but your discussion below does use "xor_offset" with underscores.)

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 81

Quoted 8 lines
Documentation/technical/bitmap-format.txt +8 -0
+			triplets. The format and meaning of the table is described
+			below.
++
+NOTE: This xor_offset is different from the bitmap's xor_offset.
+Bitmap's xor_offset is relative i.e. it tells how many bitmaps we have
+to go back from the current bitmap. Lookup table's xor_offset tells the
+position of the triplet in the list whose bitmap the current commit's
+bitmap have to xor with.

I found this difficult to parse. Here is an attempt at a rewording. Please let me know if I misunderstood something when reading your version:

NOTE: The xor_offset stored in the BITMAP_OPT_LOOKUP_TABLE is different from the xor_offset used in the bitmap data table. The xor_offset in this table indicates the row number within this table of the commit whose bitmap is used for the XOR computation with the current commit's stored bitmap to create the proper logical reachability bitmap.

This does make me think that "xor_offset" should really be "xor_row" or something like that.

Quoted 15 lines
Documentation/technical/bitmap-format.txt +8 -0
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +218,31 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last `N * (4 + 8 + 4)`
+(preceding the name-hash cache and trailing hash) of the `.bitmap` file
+contains a lookup table specifying the information needed to get the
+desired bitmap from the entries without parsing previous unnecessary

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 238

Quoted 10 lines
Documentation/technical/bitmap-format.txt +10 -0
+bitmaps.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the table
+contains a list of `nr_entries` <commit pos, offset, xor offset> triplets.
+The content of i'th triplet is -
+
+	* {empty}
+	commit pos (4 byte integer, network byte order): ::
+	It stores the object position of the commit (in the midx or pack index)
+	to which the i'th bitmap in the bitmap entries belongs.

Ok, we are saving some space here, but relying on looking into the pack-index or multi-pack-index to get the actual commit OID.

Since this is sorted by the order that stores the bitmaps, binary search will no longer work on this list (unless we enforce that on the rest of the bitmap file). I am going to expect that you parse this table into a hashmap in order to allow fast commit lookups. I'll keep an eye out for that implementation.

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 248

Quoted 9 lines
Documentation/technical/bitmap-format.txt +9 -0
+	* {empty}
+	offset (8 byte integer, network byte order): ::
+	The offset from which that commit's bitmap can be read.
+
+	* {empty}
+	xor offset (4 byte integer, network byte order): ::
+	It holds the position of the triplet with whose bitmap the
+	current bitmap need to xor. If the current triplet's bitmap
+	do not have any xor bitmap, it defaults to 0xffffffff.

This last sentence seems backward. Perhaps:

If the value is 0xffffffff, then the current bitmap has no xor bitmap.

Thanks,
-Stolee

#38 lore raw

Re: [PATCH v2 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 3 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documentated by an earlier

s/documentated/documented/

Quoted 1 line

change, but Git does not yet knowhow to write that extension.

s/knowhow/know how/

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 684

Quoted 4 lines
pack-bitmap-write.c +4 -0
+static int table_cmp(const void *_va, const void *_vb, void *commit_positions)
+{
+	int8_t result = 0;
+	uint32_t *positions = (uint32_t *) commit_positions;

nit: drop the space between the cast and commit_positions.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 696

Quoted 12 lines
pack-bitmap-write.c +12 -0
+	uint32_t a = positions[*(uint32_t *)_va];
+	uint32_t b = positions[*(uint32_t *)_vb];
+
+	if (a > b)
+		result = 1;
+	else if (a < b)
+		result = -1;
+	else
+		result = 0;
+
+	return result;
+}

Ok, here you are sorting by commit OID (indirectly by the order in the [multi-]pack-index). I suppose that I misunderstood in the previous patch, so that could use some more specific language, maybe.

Quoted 4 lines
pack-bitmap-write.c +4 -0
+static void write_lookup_table(struct hashfile *f,
+			       uint64_t *offsets,
+			       uint32_t *commit_positions)
+{

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 711

Quoted 10 lines
pack-bitmap-write.c +10 -0
+	uint32_t i;
+	uint32_t *table, *table_inv;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);

At the end of this sort, table[j] = i means that the ith bitmap corresponds to the jth bitmapped commit in lex order of OIDs.

Review

Quoted 2 lines
(quoted diff) +2 -0
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;

And table_inv helps us discover that relationship (ith bitmap to jth commit by j = table_inv[i]).

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 718

Quoted 3 lines
pack-bitmap-write.c +3 -0
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		uint32_t xor_offset = selected->xor_offset;

Here, xor_offset is "number of bitmaps in relationship to the current bitmap"

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 723

Quoted 4 lines
pack-bitmap-write.c +4 -0
+		hashwrite_be32(f, commit_positions[table[i]]);
+		hashwrite_be64(f, offsets[table[i]]);
+		hashwrite_be32(f, xor_offset ?
+				table_inv[table[i] - xor_offset]: 0xffffffff);

Which means that if "k = table[i] - xor_offset" that the xor base is the kth bitmap. table_inv[k] gets us the position in this table of that bitmap's commit.

(It's also strange to me that the offset is being _subtracted_, but I guess the bitmap format requires the xor base to appear first so the offset does not need to be a negative number ever.)

This last line is a bit complex.

	uint32_t xor_offset = selected->xor_offset;
	uint32_t xor_row = 0xffffffff;

	if (xor_offset) {
		uint32_t xor_order = table[i] - xor_offset;
		xor_row = table_inf[xor_order];
	}

...then we can "hashwrite_be32(f, xor_row);" when necessary. I'm not sure that we need the "uint32_t xor_order" inside the "if (xor_offset)" block, but splitting it helps add clarity to the multi-step computation.

pack-bitmap.h struct bitmap_disk_header { · line 30

Quoted 7 lines

enum pack_bitmap_opts {

pack-bitmap.h +3 -2
-	BITMAP_OPT_FULL_DAG = 1,
-	BITMAP_OPT_HASH_CACHE = 4,
+	BITMAP_OPT_FULL_DAG = 0x1,
+	BITMAP_OPT_HASH_CACHE = 0x4,
+	BITMAP_OPT_LOOKUP_TABLE = 0x10,
 };

Excellent.

Thanks,
-Stolee

#44 lore raw

Re: [PATCH v2 3/6] pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 5 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Teach git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'. Default is true.

I wonder if it makes sense to have it default to 'false' for now, but to change that default after the feature has been shipped and running in production for a while.

Quoted 1 line

Also add test to verify writting of lookup table.

s/writting/writing/

Documentation/config/pack.txt When writing a multi-pack reachability bitmap, no new namehashes are · line 168

Quoted 2 lines
Documentation/config/pack.txt +2 -0
+pack.writeBitmapLookupTable::
+	When true, git will include a "lookup table" section in the

I think you should either use "Git" when talking about the software generally, OR use "git repack --write-bitmap-index will include..."

Documentation/config/pack.txt When writing a multi-pack reachability bitmap, no new namehashes are · line 171

Quoted 3 lines
Documentation/config/pack.txt +3 -0
+	bitmap index (if one is written). This table is used to defer
+	loading individual bitmaps as late as possible. This can be
+	beneficial in repositories which have relatively large bitmap

s/which/that/

(I'm pretty sure that "that" is better. We're trying to restrict the set of repositories we are talking about, not implying that all repositories have this property.)

Quoted 14 lines
Documentation/config/pack.txt +2 -0
+	indexes. Defaults to true.
+
pack-bitmap-write.c +1 -0
@@ -713,6 +713,7 @@ static void write_lookup_table(struct hashfile *f,
 	for (i = 0; i < writer.selected_nr; i++)
 		table_inv[table[i]] = i;
 
+	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
 	for (i = 0; i < writer.selected_nr; i++) {
 		struct bitmapped_commit *selected = &writer.selected[table[i]];
 		uint32_t xor_offset = selected->xor_offset;

pack-bitmap-write.c static void write_lookup_table(struct hashfile *f, · line 730

Quoted 5 lines
pack-bitmap-write.c +1 -0
@@ -725,6 +726,7 @@ static void write_lookup_table(struct hashfile *f,
 
 	free(table);
 	free(table_inv);
+	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
 }

These lines seem misplaced. Maybe they were meant for the previous patch?

Thanks,
-Stolee

#51 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 5 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Earlier change teaches Git to write bitmap lookup table. But Git does not know how to parse them.

Quoted 7 lines

Teach Git to parse the existing bitmap lookup table. The older versions of git are not affected by it. Those versions ignore the lookup table.

I didn't check the previous patches, but your sign-off should be the last line of the message. (You are singing off on all previous content, and any later content is not covered by your sign-off.)

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 196

Quoted 3 lines
pack-bitmap.c +3 -0
+
+		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
+			git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {

nit: This alignment should use four spaces at the end so the second phrase matches the start of the previous phrase. Like this:

		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
		    git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {

Perhaps it looked right in your editor because it renders tabs as 4 spaces instead of 8 spaces.

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 200

Quoted 4 lines
pack-bitmap.c +4 -0
+			size_t table_size = 0;
+			size_t triplet_sz = st_add3(sizeof(uint32_t),    /* commit position */
+							sizeof(uint64_t),    /* offset */
+							sizeof(uint32_t));    /* xor offset */

The 4- vs 8-space tab view would also explain the alignment here:

			size_t triplet_sz = st_add3(sizeof(uint32_t),  /* commit position */
						    sizeof(uint64_t),  /* offset */
						    sizeof(uint32_t)); /* xor offset */

(I also modified the comment alignment.)

Of course, since these values are constants and have no risk of overflowing, perhaps we can drop st_add3() here:

			size_t triplet_sz = sizeof(uint32_t) + /* commit position */
					    sizeof(uint64_t) +  /* offset */
					    sizeof(uint32_t); /* xor offset */

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 204

Quoted 3 lines
pack-bitmap.c +3 -0
+			table_size = st_add(table_size,
+					st_mult(ntohl(header->entry_count),
+						triplet_sz));

Here, we _do_ want to keep the st_mult(). Is the st_add() still necessary? It seems this is a leftover from the previous version that had the 4-byte flag data.

We set table_size to zero above. We could drop that initialization and instead have this after the "size_t triplet_sz" definition:

			size_t table_size = st_mult(ntohl(header->entry_count),
						    triplet_sz));

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 206

Quoted 2 lines
pack-bitmap.c +2 -0
+			if (table_size > index_end - index->map - header_size)
+				return error("corrupted bitmap index file (too short to fit lookup table)");

Please add "_(...)" around the error message so it can be translated.

Quoted 5 lines
pack-bitmap.c +3 -1
+			index->table_lookup = (void *)(index_end - table_size);
+			index_end -= table_size;
+		}
 
-	/* a 0 return code means the insertion succeeded with no changes,

pack-bitmap.c static struct stored_bitmap *store_bitmap(struct bitmap_index *index, · line 243

Quoted 10 lines
pack-bitmap.c +8 -2
-	 * because the SHA1 already existed on the map. this is bad, there
-	 * shouldn't be duplicated commits in the index */
+	/* A 0 return code means the insertion succeeded with no changes,
+	 * because the SHA1 already existed on the map. If lookup table
+	 * is NULL, this is bad, there shouldn't be duplicated commits
+	 * in the index.
+	 *
+	 * If table_lookup exists, that means the desired bitmap is already
+	 * loaded. Either this bitmap has been stored directly or another
+	 * bitmap has a direct or indirect xor relation with it. */

If we are modifying this multi-line comment, then we should reformat it to match convention:

/*
* The first sentence starts after the comment start
* so it has symmetry with the comment end which is on
* its own line.
*/

pack-bitmap.c static struct stored_bitmap *store_bitmap(struct bitmap_index *index, · line 246

Quoted 5 lines
pack-bitmap.c +2 -2
 	if (ret == 0) {
-		error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
-		return NULL;
+		if (!index->table_lookup) {
+			error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));

Errors start with lowercase letters. Please add translation markers "_(...)"

pack-bitmap.c struct include_data { · line 605

Quoted 3 lines
pack-bitmap.c +3 -0
+static uint32_t triplet_get_xor_pos(const void *triplet)
+{
+	const void *p = (unsigned char*) triplet + st_add(sizeof(uint32_t), sizeof(uint64_t));

This st_add() is not necessary since the constants will not overflow.

Quoted 10 lines
pack-bitmap.c +10 -0
+	return get_be32(p);
+}
+
+static int triplet_cmp(const void *va, const void *vb)
+{
+	int result = 0;
+	uint32_t *a = (uint32_t *) va;
+	uint32_t b = get_be32(vb);
+	if (*a > b)
+		result = 1;

pack-bitmap.c struct include_data { · line 625

Quoted 10 lines
pack-bitmap.c +10 -0
+	else if (*a < b)
+		result = -1;
+	else
+		result = 0;
+
+	return result;
+}
+
+static uint32_t bsearch_pos(struct bitmap_index *bitmap_git, struct object_id *oid,
+						uint32_t *result)

Strange wrapping. Perhaps

static uint32_t bsearch_pos(struct bitmap_index *bitmap_git,

			    struct object_id *oid,
			    uint32_t *result)

pack-bitmap.c struct include_data { · line 634

Quoted 9 lines
pack-bitmap.c +9 -0
+{
+	int found;
+
+	if (bitmap_git->midx)
+		found = bsearch_midx(oid, bitmap_git->midx, result);
+	else
+		found = bsearch_pack(oid, bitmap_git->pack, result);
+
+	return found;

Here, we are doing a binary search on the entire list of packed objects, which could use quite a few more hops than a binary search on the bitmapped commits.

Quoted 2 lines
pack-bitmap.c +2 -0
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+					  struct commit *commit)

...

pack-bitmap.c struct include_data { · line 655

Quoted 7 lines
pack-bitmap.c +7 -0
+	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
+
+	if (!found)
+		return NULL;
+
+	triplet = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+						triplet_sz, triplet_cmp);

But I see, you are searching the pack-index for the position in the index, and _then_ searching the bitmap lookup table based on that position value.

I expected something different: binary search on the triplets where the comparison is made by looking up the OID from the [multi-]pack-index and comparing that OID to the commit OID we are looking for.

I'm not convinced that the binary search I had in mind is meaningfully faster than what you've implemented here, so I'm happy to leave it as you have it. We can investigate if that full search on the pack-index matters at all (it probably doesn't).

Quoted 4 lines
pack-bitmap.c +4 -0
+	if (!triplet)
+		return NULL;
+
+	offset = triplet_get_offset(triplet);

pack-bitmap.c struct include_data { · line 669

Quoted 10 lines
pack-bitmap.c +10 -0
+	xor_pos = triplet_get_xor_pos(triplet);
+
+	if (xor_pos != 0xffffffff) {
+		int xor_flags;
+		uint64_t offset_xor;
+		uint32_t *xor_positions;
+		struct object_id xor_oid;
+		size_t size = 0;
+
+		ALLOC_ARRAY(xor_positions, bitmap_git->entry_count);

While there is potential that this is wasteful, it's probably not that huge, so we can start with the "maximum XOR depth" and then reconsider a smaller allocation in the future.

pack-bitmap.c struct include_data { · line 670

Quoted 1 line
pack-bitmap.c +1 -0
+		while (xor_pos != 0xffffffff) {

We should consider ensuring that also "size < bitmap_git->entry_count". Better yet, create an xor_positions_alloc variable that is initialized to the entry_count value.

"size" should probably be xor_positions_nr.

pack-bitmap.c struct include_data { · line 674

Quoted 4 lines
pack-bitmap.c +4 -0
+			xor_positions[size++] = xor_pos;
+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
+			xor_pos = triplet_get_xor_pos(triplet);
+		}

(at this point, "if (xor_positions_nr >= xor_positions_alloc)", then error out since the file must be malformed with an XOR loop.)

pack-bitmap.c struct include_data { · line 676

Quoted 1 line
pack-bitmap.c +1 -0
+		while (size){

nit: ") {"

Quoted 5 lines
pack-bitmap.c +5 -0
+			xor_pos = xor_positions[size - 1];
+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
+			commit_pos = get_be32(triplet);
+			offset_xor = triplet_get_offset(triplet);
+

pack-bitmap.c struct include_data { · line 691

Quoted 10 lines
pack-bitmap.c +10 -0
+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, commit_pos) < 0) {
+				free(xor_positions);
+				return NULL;
+			}
+
+			bitmap_git->map_pos = offset_xor + sizeof(uint32_t) + sizeof(uint8_t);
+			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+			bitmap = read_bitmap_1(bitmap_git);
+
+			if (!bitmap){

nit: ") {"

pack-bitmap.c struct include_data { · line 696

Quoted 5 lines
pack-bitmap.c +5 -0
+				free(xor_positions);
+				return NULL;
+			}
+
+			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_oid, xor_bitmap, xor_flags);

Since we are storing the bitmap here as we "pop" the stack, should we be looking for a stored bitmap while pushing to the stack in the previous loop? That would save time when using multiple bitmaps with common XOR bases.

(Of course, we want to be careful that we do not create a recursive loop, but instead _only_ look at the in-memory bitmaps that already exist.)

Quoted 6 lines
pack-bitmap.c +6 -0
+			size--;
+		}
+
+		free(xor_positions);
+	}
+

pack-bitmap.c struct include_data { · line 712

Quoted 10 lines
pack-bitmap.c +10 -0
+	bitmap_git->map_pos = offset + sizeof(uint32_t) + sizeof(uint8_t);
+	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+	bitmap = read_bitmap_1(bitmap_git);
+
+	if (!bitmap)
+		return NULL;
+
+	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);
+}
+

I'm happy with the structure of this iterative algorithm!

I'll pause my review here for now.

Thanks,
-Stolee

#35 lore raw

Re: [PATCH v2 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Derrick Stolee
Quoted 13 lines · Derrick Stolee

Documentation/technical/bitmap-format.txt MIDXs, both the bit-cache and rev-cache extensions are required. · line 81

Quoted 8 lines · Abhradeep Chakraborty via GitGitGadget
Documentation/technical/bitmap-format.txt +8 -0
+			triplets. The format and meaning of the table is described
+			below.
++
+NOTE: This xor_offset is different from the bitmap's xor_offset.
+Bitmap's xor_offset is relative i.e. it tells how many bitmaps we have
+to go back from the current bitmap. Lookup table's xor_offset tells the
+position of the triplet in the list whose bitmap the current commit's
+bitmap have to xor with.

I found this difficult to parse. Here is an attempt at a rewording. Please let me know if I misunderstood something when reading your version:

Quoted 8 lines

NOTE: The xor_offset stored in the BITMAP_OPT_LOOKUP_TABLE is different from the xor_offset used in the bitmap data table. The xor_offset in this table indicates the row number within this table of the commit whose bitmap is used for the XOR computation with the current commit's stored bitmap to create the proper logical reachability bitmap.

This does make me think that "xor_offset" should really be "xor_row" or something like that.

To be fair, I found Stolee's version equally difficult to parse. I wonder if something like the following would be clearer:

NOTE: Unlike the xor_offset used to compress an individual bitmap, this value stores an *absolute* index into the lookup table, not a location relative to the current entry.

Quoted 12 lines

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 238

Quoted 8 lines
Documentation/technical/bitmap-format.txt +8 -0
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the table
+contains a list of `nr_entries` <commit pos, offset, xor offset> triplets.
+The content of i'th triplet is -
+
+	* {empty}
+	commit pos (4 byte integer, network byte order): ::
+	It stores the object position of the commit (in the midx or pack index)
+	to which the i'th bitmap in the bitmap entries belongs.

Ok, we are saving some space here, but relying on looking into the pack-index or multi-pack-index to get the actual commit OID.

Quoted 4 lines

Since this is sorted by the order that stores the bitmaps, binary search will no longer work on this list (unless we enforce that on the rest of the bitmap file). I am going to expect that you parse this table into a hashmap in order to allow fast commit lookups. I'll keep an eye out for that implementation.

The main purpose of this series is to avoid having to construct such a table ahead of time. This is more or less akin to what the existing implementation already does in load_bitmap_entries_v1(), though that function has to read (but not decompress!) all bitmaps.

But I disagree that this isn't binary searchable. The object positions are in MIDX or pack .idx order, so they are sorted lexicographically. The comparator implementation could either take as its key an object_id, and then convert each of the "commit pos" fields themselves to object_ids and call oidcmp().

Or we could go the other way (as it looks like Abhradeep did in a later patch) and convert the key's object_id into the index or MIDX-relative position, and search for that.

Quoted 12 lines

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 248

Quoted 9 lines
Documentation/technical/bitmap-format.txt +9 -0
+	* {empty}
+	offset (8 byte integer, network byte order): ::
+	The offset from which that commit's bitmap can be read.
+
+	* {empty}
+	xor offset (4 byte integer, network byte order): ::
+	It holds the position of the triplet with whose bitmap the
+	current bitmap need to xor. If the current triplet's bitmap
+	do not have any xor bitmap, it defaults to 0xffffffff.

This last sentence seems backward. Perhaps:

Quoted 1 line

If the value is 0xffffffff, then the current bitmap has no xor bitmap.

Perhaps even more concisely:

The position of a triplet whose bitmap is used to compress this one, or 0xffffffff if no such bitmap exists.

Thanks,
Taylor

#41 lore raw

Re: [PATCH v2 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 33 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documentated by an earlier change, but Git does not yet knowhow to write that extension.

Teach git to write bitmap lookup table extension. The table contains the list of N <commit pos, offset, xor offset>` triplets. These triplets are sorted according to their commit pos (ascending order). The meaning of each data in the i'th triplet is given below:

- Commit pos is the position of the commit in the pack-index (or midx) to which the i'th bitmap belongs. It is a 4 byte network byte order integer.

- offset is the position of the i'th bitmap.

- xor offset denotes the position of the triplet with whose bitmap the current triplet's bitmap need to xor with.

---
 pack-bitmap-write.c | 72 +++++++++++++++++++++++++++++++++++++++++++--
 pack-bitmap.h       |  5 ++--
 2 files changed, 73 insertions(+), 4 deletions(-)
pack-bitmap-write.c +0 -0
index c43375bd344..899a4a941e1 100644

pack-bitmap-write.c static const struct object_id *oid_access(size_t pos, const void *table) · line 654

Quoted 6 lines
pack-bitmap-write.c +2 -1
@@ -650,7 +650,9 @@ static const struct object_id *oid_access(size_t pos, const void *table)
 
 static void write_selected_commits_v1(struct hashfile *f,
 				      struct pack_idx_entry **index,
-				      uint32_t index_nr)
+				      uint32_t index_nr,
+				      uint64_t *offsets,

We should probably leave this as a pointer to an off_t, since that is a more appropriate type for keeping track of an offset within a file (and indeed it is the return type of hashfile_total()).

But since it's platform-dependent, we should make sure to cast it to a uint64_t before writing it as part of the lookup table.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 669

Quoted 10 lines
pack-bitmap-write.c +3 -0
+				      uint32_t *commit_positions)
 {
 	int i;
 
@@ -663,6 +665,11 @@ static void write_selected_commits_v1(struct hashfile *f,
 		if (commit_pos < 0)
 			BUG("trying to write commit not in index");
 
+		if (offsets)
+			offsets[i] = hashfile_total(f);

This makes sense to store here, since we can't easily recover this information later on.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 671

Quoted 2 lines
pack-bitmap-write.c +2 -0
+		if (commit_positions)
+			commit_positions[i] = commit_pos;

This one I'm not as sure about. It would be nice to not have write_selected_commits_v1() be responsible for writing this down, too. And I think it's easy enough to recover later on, since we're just doing a search over "index" (see above the "oid_pos" call).

I think that oid_pos() call could be hidden behind a function that takes an object_id pointer, an index (double pointer) of pack_idx_entry structs, and a length.

Its implementation would be something like:

    static int commit_bitmap_writer_pos(struct object_id *oid,
                                        struct pack_idx_entry **index,
                                        uint32_t index_nr)
    {
        return oid_pos(oid, index, index_nr, oid_access);
    }

and then we could replace any calls like commit_positions[i] with one that first takes i to the appropriate object_id in selected commit order.

That would be strictly less efficient, but not in a way that I think matters, and it would definitely be cleaner to not rely on a side-effect of write_selected_commits_v1().

Something in the middle there would be to have write_lookup_table() assemble that list of commit_positions itself, something like:

    uint32_t *commit_positions;

    ALLOC_ARRAY(commit_positions, writer.selected_nr);

    for (i = 0; i < writer.selected_nr; i++) {
        int pos = oid_pos(&writer.selected[i].commit->object.oid,
                          index, index_nr);
        if (pos < 0)
            BUG("trying to write commit not in index");
        commit_positions[i] = pos;
    }

    ...

    free(commit_positions);

That at least removes a side-effect from the implementation of write_selected_commits_v1() and brings the creation of the commit_positions array closer to where it's being used, while still maintaining the constant-time lookups. So that may be a good alternative, but I'm curious of your thoughts.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 681

Quoted 1 line
pack-bitmap-write.c +1 -0
+static int table_cmp(const void *_va, const void *_vb, void *commit_positions)

OK, so this is sorting the table in order of the commit positions. I would rename the commit_positions parameter to something like "void *_data", and then have commit_positions be the result of the cast, like "uint32_t *commit_positions = _data";

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 683

Quoted 2 lines
pack-bitmap-write.c +2 -0
+{
+	int8_t result = 0;

int8_t isn't an often used type in Git's codebase, but we can get rid of this variable altogether and just return immediately from each case, e.g.:

    if (a < b)
        return -1;
    else if (a > b)
        return 1;
    return 0;

or similar.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 684

Quoted 1 line
pack-bitmap-write.c +1 -0
+	uint32_t *positions = (uint32_t *) commit_positions;

Explicit cast isn't need here since you're going up from void*.

Quoted 4 lines
pack-bitmap-write.c +4 -0
+static void write_lookup_table(struct hashfile *f,
+			       uint64_t *offsets,
+			       uint32_t *commit_positions)
+{

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 711

Quoted 10 lines
pack-bitmap-write.c +10 -0
+	uint32_t i;
+	uint32_t *table, *table_inv;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);

I think the construction of table and table_inv could definitely benefit from a comment here indicating what they're used for and what they contain (e.g., "table maps abc to xyz").

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 723

Quoted 11 lines
pack-bitmap-write.c +11 -0
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;
+
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		uint32_t xor_offset = selected->xor_offset;
+
+		hashwrite_be32(f, commit_positions[table[i]]);
+		hashwrite_be64(f, offsets[table[i]]);
+		hashwrite_be32(f, xor_offset ?
+				table_inv[table[i] - xor_offset]: 0xffffffff);

Nit: missing space before ':'.

Thanks,
Taylor

#39 lore raw

Re: [PATCH v2 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Derrick Stolee
Quoted 18 lines · Derrick Stolee

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 696

Quoted 12 lines · Abhradeep Chakraborty via GitGitGadget
pack-bitmap-write.c +12 -0
+	uint32_t a = positions[*(uint32_t *)_va];
+	uint32_t b = positions[*(uint32_t *)_vb];
+
+	if (a > b)
+		result = 1;
+	else if (a < b)
+		result = -1;
+	else
+		result = 0;
+
+	return result;
+}

Ok, here you are sorting by commit OID (indirectly by the order in the [multi-]pack-index). I suppose that I misunderstood in the previous patch, so that could use some more specific language, maybe.

Yeah, I agree that some more specific language could be used, with the main idea being there that we make it clearer that the list of tuples is still sorted (and can be binary searched).

Quoted 8 lines

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 711

Quoted 4 lines
pack-bitmap-write.c +4 -0
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);

At the end of this sort, table[j] = i means that the ith bitmap corresponds to the jth bitmapped commit in lex order of OIDs.

Quoted 5 lines

Review

Quoted 2 lines
(quoted diff) +2 -0
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;

And table_inv helps us discover that relationship (ith bitmap to jth commit by j = table_inv[i]).

These are both great descriptions and should give an idea of what sort of information is worth putting into a comment.

Quoted 6 lines

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 718

Quoted 4 lines
pack-bitmap-write.c +3 -0
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		uint32_t xor_offset = selected->xor_offset;

Here, xor_offset is "number of bitmaps in relationship to the current bitmap"

It's an offset to an earlier commit which must be used to XOR-decompress the current one (if any).

Quoted 8 lines

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 723

Quoted 4 lines
pack-bitmap-write.c +4 -0
+		hashwrite_be32(f, commit_positions[table[i]]);
+		hashwrite_be64(f, offsets[table[i]]);
+		hashwrite_be32(f, xor_offset ?
+				table_inv[table[i] - xor_offset]: 0xffffffff);

Which means that if "k = table[i] - xor_offset" that the xor base is the kth bitmap. table_inv[k] gets us the position in this table of that bitmap's commit.

Yes, exactly. Abhradeep: this is also worth commenting ;-).

Quoted 3 lines

(It's also strange to me that the offset is being _subtracted_, but I guess the bitmap format requires the xor base to appear first so the offset does not need to be a negative number ever.)

You're right, this follows from the fact that the XOR bases must come before the commits who must use them to decompress themselves. From Documentation/technical/bitmap-format.txt:

This number is always positive, and hence entries are always xor'ed with **previous** bitmaps, not bitmaps that will come afterwards in the index.

Quoted 10 lines

This last line is a bit complex.

	uint32_t xor_offset = selected->xor_offset;
	uint32_t xor_row = 0xffffffff;

	if (xor_offset) {
		uint32_t xor_order = table[i] - xor_offset;
		xor_row = table_inf[xor_order];
	}
Quoted 3 lines

...then we can "hashwrite_be32(f, xor_row);" when necessary. I'm not sure that we need the "uint32_t xor_order" inside the "if (xor_offset)" block, but splitting it helps add clarity to the multi-step computation.

I had the same thought, though I would also say that xor_row should be declared, not initialized, and the "else" block of "if (xor_offset)" should set it to 0xffffffff to make the relationship between xor_offset and the value written a little clearer.

Thanks,
Taylor

#36 lore raw

Re: [PATCH v2 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Derrick Stolee
Quoted 11 lines · Derrick Stolee

I found this difficult to parse. Here is an attempt at a rewording. Please let me know if I misunderstood something when reading your version:

NOTE: The xor_offset stored in the BITMAP_OPT_LOOKUP_TABLE is different from the xor_offset used in the bitmap data table. The xor_offset in this table indicates the row number within this table of the commit whose bitmap is used for the XOR computation with the current commit's stored bitmap to create the proper logical reachability bitmap.

This does make me think that "xor_offset" should really be "xor_row" or something like that.

Thanks. xor_row seems nice to me.

Quoted 7 lines

Documentation/technical/bitmap-format.txt Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag. · line 238

Quoted 4 lines
Documentation/technical/bitmap-format.txt +4 -0
+	* {empty}
+	commit pos (4 byte integer, network byte order): ::
+	It stores the object position of the commit (in the midx or pack index)
+	to which the i'th bitmap in the bitmap entries belongs.

Ok, we are saving some space here, but relying on looking into the pack-index or multi-pack-index to get the actual commit OID.

Seems like I didn't update this particular part. At the time of writing this patch, I was clear that I would store these triplets in the bitmap's order. But when I started to implement the "read" part, I realised that these triplets need to be ordered in ascending order. So I did update the "write extension" patch but somehow missed this particular part.

Just to be clear, bitmaps are sorted by their commit's date (as far as I know). Bitmaps for recent commits comes before bitmaps for older commits. So these two orders are not same. Thus hashmap would not work here.

Will update this portion.

Thanks :)

#40 lore raw

Re: [PATCH v2 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Derrick Stolee
Quoted 18 lines · Derrick Stolee

Which means that if "k = table[i] - xor_offset" that the xor base is the kth bitmap. table_inv[k] gets us the position in this table of that bitmap's commit.

(It's also strange to me that the offset is being _subtracted_, but I guess the bitmap format requires the xor base to appear first so the offset does not need to be a negative number ever.)

This last line is a bit complex.

	uint32_t xor_offset = selected->xor_offset;
	uint32_t xor_row = 0xffffffff;

	if (xor_offset) {
		uint32_t xor_order = table[i] - xor_offset;
		xor_row = table_inf[xor_order];
	}
Quoted 3 lines

...then we can "hashwrite_be32(f, xor_row);" when necessary. I'm not sure that we need the "uint32_t xor_order" inside the "if (xor_offset)" block, but splitting it helps add clarity to the multi-step computation.

Got it. Will add comments too.

Thanks :)

#45 lore raw

PATCH v2 3/6 pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Derrick Stolee
Quoted 3 lines · Derrick Stolee

I wonder if it makes sense to have it default to 'false' for now, but to change that default after the feature has been shipped and running in production for a while.

I do not have any opinion. If most reviewers agree on it, I will surely Set it to false.

Quoted 2 lines

I think you should either use "Git" when talking about the software generally, OR use "git repack --write-bitmap-index will include..."

Ohh, yeah! Thanks for pointing out.

Quoted 5 lines

s/which/that/

(I'm pretty sure that "that" is better. We're trying to restrict the set of repositories we are talking about, not implying that all repositories have this property.)

Ok.

Quoted 2 lines

These lines seem misplaced. Maybe they were meant for the previous patch?

I mainly used it for testing purpose. That's why I included it in This patch. But I got your point and will move it to the previous patch.

Thanks :)

#47 lore raw

Re: [PATCH v2 3/6] pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 6 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Teach git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'. Default is true.

Quoted 6 lines

Also add test to verify writting of lookup table.

I think that this was covered earlier in the review of this round, but in general your Signed-off-by (often abbreviated as "S-o-b") should come last. The order should be chronological, so I'd probably suggest something like:

Quoted 19 lines
builtin/multi-pack-index.c +7 -0
index 5edbb7fe86e..3757616f09c 100644
@@ -87,6 +87,13 @@ static int git_multi_pack_index_write_config(const char *var, const char *value,
 			opts.flags &= ~MIDX_WRITE_BITMAP_HASH_CACHE;
 	}
 
+	if (!strcmp(var, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(var, value))
+			opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+		else
+			opts.flags &= ~MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+	}
+
 	/*
 	 * We should never make a fall-back call to 'git_default_config', since
 	 * this was already called in 'cmd_multi_pack_index()'.

builtin/multi-pack-index.c static int cmd_multi_pack_index_write(int argc, const char **argv) · line 133

Quoted 4 lines
builtin/multi-pack-index.c +1 -0
@@ -123,6 +130,7 @@ static int cmd_multi_pack_index_write(int argc, const char **argv)
 	};
 
 	opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE;
+	opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;

I wonder if this should respect pack.writeBitmapLookupTable, too. Probably both of them should take into account their separate configuration values, but cleaning up the hashcache one can be done separately outside of this series.

Everything else looks good.

Quoted 13 lines
pack-bitmap-write.c +1 -0
index 899a4a941e1..79be0cf80e6 100644
@@ -713,6 +713,7 @@ static void write_lookup_table(struct hashfile *f,
 	for (i = 0; i < writer.selected_nr; i++)
 		table_inv[table[i]] = i;
 
+	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
 	for (i = 0; i < writer.selected_nr; i++) {
 		struct bitmapped_commit *selected = &writer.selected[table[i]];
 		uint32_t xor_offset = selected->xor_offset;

pack-bitmap-write.c static void write_lookup_table(struct hashfile *f, · line 729

Quoted 4 lines
pack-bitmap-write.c +1 -0
@@ -725,6 +726,7 @@ static void write_lookup_table(struct hashfile *f,
 
 	free(table);
 	free(table_inv);
+	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);

This region may make more sense to include in the previous commit, though I don't have a strong feeling about it.

Thanks,
Taylor

#46 lore raw

Re: [PATCH v2 3/6] pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Abhradeep Chakraborty
Quoted 8 lines · Abhradeep Chakraborty
Quoted 3 lines · Derrick Stolee

I wonder if it makes sense to have it default to 'false' for now, but to change that default after the feature has been shipped and running in production for a while.

I do not have any opinion. If most reviewers agree on it, I will surely Set it to false.

I think it's definitely a safe approach. I don't have a huge concern about enabling it earlier, but I don't think we're in a huge rush to add a new feature here, either.

So I'd be fine to ship this with the default being disabled (IOW, *not* writing the lookup table). That should give us a window where we can shake out whatever bugs there are, as is often the case when working with the bitmap code ;).

Thanks,
Taylor

#52 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table

In reply to Derrick Stolee
Quoted 3 lines · Derrick Stolee

I didn't check the previous patches, but your sign-off should be the last line of the message. (You are singing off on all previous content, and any later content is not covered by your sign-off.)

Ohhh, got it. I didn't know about it before.

Quoted 8 lines

nit: This alignment should use four spaces at the end so the second phrase matches the start of the previous phrase. Like this:

		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
		    git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {

Perhaps it looked right in your editor because it renders tabs as 4 spaces instead of 8 spaces.

I don't know why but my editor sometimes do some weird things for alignments. I generally use VS Code. But for alignment related problems, sometimes I have to use vi editor.

Quoted 9 lines

Here, we _do_ want to keep the st_mult(). Is the st_add() still necessary? It seems this is a leftover from the previous version that had the 4-byte flag data.

We set table_size to zero above. We could drop that initialization and instead have this after the "size_t triplet_sz" definition:

			size_t table_size = st_mult(ntohl(header->entry_count),
						    triplet_sz));

Yes, you're right. Will update.

Quoted 7 lines

I expected something different: binary search on the triplets where the comparison is made by looking up the OID from the [multi-]pack-index and comparing that OID to the commit OID we are looking for.

I'm not convinced that the binary search I had in mind is meaningfully faster than what you've implemented here, so I'm happy to leave it as you have it. We can investigate if that full search on the pack-index matters at all (it probably doesn't).

Good idea! Thanks!

Quoted 3 lines

While there is potential that this is wasteful, it's probably not that huge, so we can start with the "maximum XOR depth" and then reconsider a smaller allocation in the future.

Ok.

Quoted 6 lines

We should consider ensuring that also "size < bitmap_git->entry_count". Better yet, create an xor_positions_alloc variable that is initialized to the entry_count value.

"size" should probably be xor_positions_nr.

Quoted 7 lines

pack-bitmap.c struct include_data { · line 674

Quoted 4 lines
pack-bitmap.c +4 -0
+			xor_positions[size++] = xor_pos;
+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
+			xor_pos = triplet_get_xor_pos(triplet);
+		}

(at this point, "if (xor_positions_nr >= xor_positions_alloc)", then error out since the file must be malformed with an XOR loop.)

Got it.

Quoted 3 lines

Since we are storing the bitmap here as we "pop" the stack, should we be looking for a stored bitmap while pushing to the stack in the previous loop? That would save time when using multiple bitmaps with common XOR bases.

Yeah, I also am thinking about it. Will make a try.

Thanks :)

#42 lore raw

Re: [PATCH v2 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Taylor Blau
Quoted 6 lines · Taylor Blau

We should probably leave this as a pointer to an off_t, since that is a more appropriate type for keeping track of an offset within a file (and indeed it is the return type of hashfile_total()).

But since it's platform-dependent, we should make sure to cast it to a uint64_t before writing it as part of the lookup table.

Hmm, will make the necessary changes.

Quoted 5 lines

That at least removes a side-effect from the implementation of write_selected_commits_v1() and brings the creation of the commit_positions array closer to where it's being used, while still maintaining the constant-time lookups. So that may be a good alternative, but I'm curious of your thoughts.

Sounds good to me :)

Quoted 3 lines

I think the construction of table and table_inv could definitely benefit from a comment here indicating what they're used for and what they contain (e.g., "table maps abc to xyz").

Yeah, true. Will add comments.

Thanks for the other suggestions also :)

#53 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table

In reply to Abhradeep Chakraborty
Quoted 7 lines · Abhradeep Chakraborty
Quoted 6 lines · Derrick Stolee

nit: This alignment should use four spaces at the end so the second phrase matches the start of the previous phrase. Like this:

		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
		    git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {
Quoted 6 lines
Quoted 2 lines

Perhaps it looked right in your editor because it renders tabs as 4 spaces instead of 8 spaces.

I don't know why but my editor sometimes do some weird things for alignments. I generally use VS Code. But for alignment related problems, sometimes I have to use vi editor.

I also use VS Code, and I noticed a few spacing issues recently, especially in .txt files.

I submitted a patch [1] to improve the contrib/vscode/init.sh script, which adds some helpful config settings to your Git workspace. Please take a look and see how it works for you.

Thanks,
-Stolee

[1] https://lore.kernel.org/git/[email protected]

#48 lore raw

Re: [PATCH v2 3/6] pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Taylor Blau
Quoted 3 lines · Taylor Blau

Probably both of them should take into account their separate configuration values, but cleaning up the hashcache one can be done separately outside of this series.

Actually, it does respect the pack.writebitmaplookuptable config. As pack.writebitmaplookuptable is by default true (for this patch Series), this line enables it by default. If pack.writebitmaplookuptable Set to false, the proposed change in the git_multi_pack_index_write_config function disables this flag.

Quoted 2 lines

This region may make more sense to include in the previous commit, though I don't have a strong feeling about it.

Ok. Will move it to the previous patch.

Thanks :)

#58 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 7 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Earlier change teaches Git to write bitmap lookup table. But Git does not know how to parse them.

Teach Git to parse the existing bitmap lookup table. The older versions of git are not affected by it. Those versions ignore the

s/git/Git

Quoted 16 lines

lookup table.

---
 pack-bitmap.c                 | 193 ++++++++++++++++++++++++++++++++--
 t/t5310-pack-bitmaps.sh       |   7 ++
 t/t5326-multi-pack-bitmaps.sh |   1 +
 3 files changed, 191 insertions(+), 10 deletions(-)
pack-bitmap.c +0 -0
index 36134222d7a..9e09c5824fc 100644

pack-bitmap.c struct bitmap_index { · line 87

Quoted 6 lines
pack-bitmap.c +3 -0
@@ -82,6 +82,12 @@ struct bitmap_index {
 	/* The checksum of the packfile or MIDX; points into map. */
 	const unsigned char *checksum;
 
+	/*
+	 * If not NULL, this point into the commit table extension
+	 * (within map).

It may be worth replacing "within map" to "within the memory mapped region map" to make clear that this points somewhere within the mmap.

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 196

Quoted 12 lines
pack-bitmap.c +6 -0
+	 */
+	unsigned char *table_lookup;
+
 
 
@@ -185,6 +191,22 @@ static int load_bitmap_header(struct bitmap_index *index)
 			index->hashes = (void *)(index_end - cache_size);
 			index_end -= cache_size;
 		}
+
+		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
+			git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {

I should have commented on this in an earlier round, but I wonder what the behavior should be when we have BITMAP_OPT_LOOKUP_TABLE in our flags, but GIT_TEST_READ_COMMIT_TABLE is disabled.

Right now, it doesn't matter, since there aren't any flags in bits above BITMAP_OPT_LOOKUP_TABLE. But in the future, if there was some BITMAP_OPT_FOO that was newer than BITMAP_OPT_LOOKUP_TABLE, we would want to be able to read it without needing to read the lookup table.

At least, I think that should be true, though I would be interested to hear if anybody has a differing opinion there.

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 200

Quoted 4 lines
pack-bitmap.c +4 -0
+			size_t table_size = 0;
+			size_t triplet_sz = st_add3(sizeof(uint32_t),    /* commit position */
+							sizeof(uint64_t),    /* offset */
+							sizeof(uint32_t));    /* xor offset */

I don't think we need a st_add3() call here, since the size of these three types is known to be small and thus won't overflow the available range of size_t.

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 204

Quoted 3 lines
pack-bitmap.c +3 -0
+			table_size = st_add(table_size,
+					st_mult(ntohl(header->entry_count),
+						triplet_sz));

And table_size here is going to start off at zero, so the outer st_add() call isn't necessary, either. This should instead be:

    size_t table_size = st_mult(ntohl(header->entry_count),
                                sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint32_t));

It might be nice to have triplet_sz #define'd somewhere else, since there are a handful of declarations in this patch that are all identical. Probably something like:

    #define BITMAP_LOOKUP_TABLE_RECORD_WIDTH (sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uin32_t))

or even:

    /*
     * The width in bytes of a single record in the lookup table
     * extension:
     *
     *   (commit_pos, offset, xor_pos)
     *
     * whose fields are 32-, 64-, and 32-bits wide, respectively.
     */
    #define BITMAP_LOOKUP_TABLE_RECORD_WIDTH (16)

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 206

Quoted 2 lines
pack-bitmap.c +2 -0
+			if (table_size > index_end - index->map - header_size)
+				return error("corrupted bitmap index file (too short to fit lookup table)");

if we decide to still recognize the lookup table extension without *reading* from it when GIT_TEST_READ_COMMIT_TABLE is unset, I think we should do something like:

    if (git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1))
        index->table_lookup = (void *)(index_end - table_size);
    index_end -= table_size;

...where the subtraction on index_end happens unconditionally.

pack-bitmap.c struct include_data { · line 592

Quoted 3 lines
pack-bitmap.c +3 -0
+static inline const void *bitmap_get_triplet(struct bitmap_index *bitmap_git, uint32_t xor_pos)
+{
+	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));

Same note about the #define constant here.

pack-bitmap.c struct include_data { · line 593

Quoted 1 line
pack-bitmap.c +1 -0
+	const void *p = bitmap_git->table_lookup + st_mult(xor_pos, triplet_sz);

And this can be returned directly. Just:

    return bitmap_git->table_lookup + st_mult(xor_pos, BITMAP_LOOKUP_TABLE_RECORD_WIDTH);

although I wonder: why "xor_pos" and not just "pos" here?

pack-bitmap.c struct include_data { · line 607

Quoted 11 lines
pack-bitmap.c +11 -0
+static uint64_t triplet_get_offset(const void *triplet)
+{
+	const void *p = (unsigned char*) triplet + sizeof(uint32_t);
+	return get_be64(p);
+}
+
+static uint32_t triplet_get_xor_pos(const void *triplet)
+{
+	const void *p = (unsigned char*) triplet + st_add(sizeof(uint32_t), sizeof(uint64_t));
+	return get_be32(p);
+}

I wonder if we could get rid of these functions altogether and return a small structure like:

    struct bitmap_lookup_table_record {
        uint32_t commit_pos;
        uint64_t offset;
        uint32_t xor_pos;
    };

or similar.

pack-bitmap.c struct include_data { · line 613

Quoted 5 lines
pack-bitmap.c +5 -0
+static int triplet_cmp(const void *va, const void *vb)
+{
+	int result = 0;
+	uint32_t *a = (uint32_t *) va;
+	uint32_t b = get_be32(vb);

Hmm. This is a little tricky to read. Here we're expecting "va" to hold commit_pos from below, and "vb" to be a pointer at a lookup record. Everything here is right, though I wonder if a comment or two might clarify why one is "*(uint32_t *)va" and the other is "get_be32(vb)".

pack-bitmap.c struct include_data { · line 619

Quoted 6 lines
pack-bitmap.c +6 -0
+	if (*a > b)
+		result = 1;
+	else if (*a < b)
+		result = -1;
+	else
+		result = 0;

Let's just return the result of the comparison directly here. And while I'm looking at it, I think we can avoid dereferencing "a" on each use, and instead just dereference va on assignment after casting, e.g.:

    uint32_t a = *(uint32_t*)va;

pack-bitmap.c struct include_data { · line 629

Quoted 6 lines
pack-bitmap.c +6 -0
+static uint32_t bsearch_pos(struct bitmap_index *bitmap_git, struct object_id *oid,
+						uint32_t *result)
+{
+	int found;
+
+	if (bitmap_git->midx)

Nit: let's use the bitmap_is_midx() helper here instead of looking at bitamp_git->midx directly.

pack-bitmap.c struct include_data { · line 635

Quoted 6 lines
pack-bitmap.c +6 -0
+		found = bsearch_midx(oid, bitmap_git->midx, result);
+	else
+		found = bsearch_pack(oid, bitmap_git->pack, result);
+
+	return found;
+}

Makes sense.

Quoted 11 lines
pack-bitmap.c +11 -0
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+					  struct commit *commit)
+{
+	uint32_t commit_pos, xor_pos;
+	uint64_t offset;
+	int flags;
+	const void *triplet = NULL;
+	struct object_id *oid = &commit->object.oid;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap = NULL;
+	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));

pack-bitmap.c struct include_data { · line 657

Quoted 10 lines
pack-bitmap.c +10 -0
+
+	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
+
+	if (!found)
+		return NULL;
+
+	triplet = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+						triplet_sz, triplet_cmp);
+	if (!triplet)
+		return NULL;

OK. If you don't mind, I'm going to "think aloud" while I read through this function to make sure that we're on the same page.

First thing is to convert the commit OID we're looking for into its position within the corresponding pack index or MIDX file so that we can use it as a search key to locate in the lookup table. If we didn't find anything, or the commit doesn't exist in our pack / MIDX, nothing to do.

pack-bitmap.c struct include_data { · line 660

Quoted 3 lines
pack-bitmap.c +3 -0
+
+	offset = triplet_get_offset(triplet);
+	xor_pos = triplet_get_xor_pos(triplet);

Otherwise, record its offset and XOR "offset".

pack-bitmap.c struct include_data { · line 669

Quoted 9 lines
pack-bitmap.c +9 -0
+
+	if (xor_pos != 0xffffffff) {
+		int xor_flags;
+		uint64_t offset_xor;
+		uint32_t *xor_positions;
+		struct object_id xor_oid;
+		size_t size = 0;
+
+		ALLOC_ARRAY(xor_positions, bitmap_git->entry_count);

If we are XOR'd with another bitmap, make a stack of those bitmaps so that we can decompress ourself.

I'm a little surprised that we're allocating an array as large as bitmap_git->entry_count. It's not wrong, but it does waste some bytes since we likely don't often have these long chains of XOR'd bitmaps.

We should instead allocate a smaller array and grow it over time (search for examples of ALLOC_GROW() to see the canonical way to do this in Git's codebase).

pack-bitmap.c struct include_data { · line 676

Quoted 7 lines
pack-bitmap.c +7 -0
+		while (xor_pos != 0xffffffff) {
+			xor_positions[size++] = xor_pos;
+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
+			xor_pos = triplet_get_xor_pos(triplet);
+		}
+
+		while (size){

Nit: missing space after ")" and before "{".

pack-bitmap.c struct include_data { · line 678

Quoted 2 lines
pack-bitmap.c +2 -0
+			xor_pos = xor_positions[size - 1];
+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);

We already have to get the triplets in the loop above, and then we dig them back out here. Would it be easier to keep track of a list of pointers into the mmaped region instead of looking up these triplets each time?

pack-bitmap.c struct include_data { · line 682

Quoted 4 lines
pack-bitmap.c +4 -0
+			commit_pos = get_be32(triplet);
+			offset_xor = triplet_get_offset(triplet);
+
+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, commit_pos) < 0) {

Should it be an error if we can't look up the object's ID here? I'd think so.

pack-bitmap.c struct include_data { · line 691

Quoted 9 lines
pack-bitmap.c +9 -0
+				free(xor_positions);
+				return NULL;
+			}
+
+			bitmap_git->map_pos = offset_xor + sizeof(uint32_t) + sizeof(uint8_t);
+			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+			bitmap = read_bitmap_1(bitmap_git);
+
+			if (!bitmap){

Nit: missing space between ")" and "{".

pack-bitmap.c struct include_data { · line 697

Quoted 6 lines
pack-bitmap.c +6 -0
+				free(xor_positions);
+				return NULL;
+			}
+
+			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_oid, xor_bitmap, xor_flags);
+			size--;

Makes sense. Nicely done!

pack-bitmap.c struct include_data { · line 705

Quoted 8 lines
pack-bitmap.c +8 -0
+		}
+
+		free(xor_positions);
+	}
+
+	bitmap_git->map_pos = offset + sizeof(uint32_t) + sizeof(uint8_t);
+	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+	bitmap = read_bitmap_1(bitmap_git);

Great, and now we can finally read the original bitmap that we wanted to...

pack-bitmap.c struct include_data { · line 710

Quoted 4 lines
pack-bitmap.c +4 -0
+	if (!bitmap)
+		return NULL;
+
+	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);

...and XOR it with the thing we built up in the loop. Very nicely done. Do we have a good way to make sure that we're testing this code in CI? It *seems* correct to me, but of course, we should have a computer check that this produces OK results, not a human ;).

Quoted 5 lines
pack-bitmap.c +2 -0
+}
+
 struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
 				      struct commit *commit)
 {

pack-bitmap.c struct include_data { · line 723

Quoted 10 lines
pack-bitmap.c +6 -2
 	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
 					   commit->object.oid);
-	if (hash_pos >= kh_end(bitmap_git->bitmaps))
-		return NULL;
+	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
+		struct stored_bitmap *bitmap = NULL;
+		if (!bitmap_git->table_lookup)
+			return NULL;
+
+		/* NEEDSWORK: cache misses aren't recorded */

For what it's worth, I think that it's completely fine to leave this as a NEEDSWORK for the purposes of this series. I think we plausibly could improve this in certain scenarios by finding some threshold on cache misses when we should just fault in all bitmaps, but that can easily be done on top.

pack-bitmap.c struct include_data { · line 725

Quoted 2 lines
pack-bitmap.c +2 -0
+		bitmap = lazy_bitmap_for_commit(bitmap_git, commit);
+		if(!bitmap)

Nit: missing space between "if" and "(".

Quoted 8 lines
pack-bitmap.c +3 -0
+			return NULL;
+		return lookup_stored_bitmap(bitmap);
+	}
 	return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos));
 }
@@ -1699,9 +1861,13 @@ void test_bitmap_walk(struct rev_info *revs)
 	if (revs->pending.nr != 1)

pack-bitmap.c void test_bitmap_walk(struct rev_info *revs) · line 1870

Quoted 10 lines
pack-bitmap.c +5 -1
 		die("you must specify exactly one commit to test");
 
-	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
+	fprintf(stderr, "Bitmap v%d test (%d entries)\n",
 		bitmap_git->version, bitmap_git->entry_count);
 
+	if (!bitmap_git->table_lookup)
+		fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
+			bitmap_git->version, bitmap_git->entry_count);
+

I think we should probably print just one or the other here, perhaps like:

    fprintf(stderr, "Bitmap v%d test (%d entries%s)",
            bitmap_git->version,
            bitmap_git->entry_count,
            bitmap_git->table_lookup ? "" : " loaded");
Quoted 22 lines
pack-bitmap.c +7 -1
 	root = revs->pending.objects[0].item;
 	bm = bitmap_for_commit(bitmap_git, (struct commit *)root);
 
@@ -1753,10 +1919,16 @@ void test_bitmap_walk(struct rev_info *revs)
 
 int test_bitmap_commits(struct repository *r)
 {
-	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
+	struct bitmap_index *bitmap_git = NULL;
 	struct object_id oid;
 	MAYBE_UNUSED void *value;
 
+	/* As this function is only used to print bitmap selected
+	 * commits, we don't have to read the commit table.
+	 */
+	setenv("GIT_TEST_READ_COMMIT_TABLE", "0", 1);
+
+	bitmap_git = prepare_bitmap_git(r);
 	if (!bitmap_git)
 		die("failed to load bitmap indexes");
 

pack-bitmap.c int test_bitmap_commits(struct repository *r) · line 1940

Quoted 5 lines
pack-bitmap.c +1 -0
@@ -1764,6 +1936,7 @@ int test_bitmap_commits(struct repository *r)
 		printf("%s\n", oid_to_hex(&oid));
 	});
 
+	setenv("GIT_TEST_READ_COMMIT_TABLE", "1", 1);
 	free_bitmap_index(bitmap_git);

Hmm. I'm not sure I follow the purpose of tweaking GIT_TEST_READ_COMMIT_TABLE like this with setenv(). Are we trying to avoid reading the lookup table? If so, why? I'd rather avoid manipulating the environment directly like this, and instead have a function we could call to fault in all of the bitmaps (when a lookup table exists, otherwise do nothing).

Thanks,
Taylor

#54 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Derrick Stolee
Quoted 9 lines · Derrick Stolee

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 204

Quoted 3 lines · Abhradeep Chakraborty via GitGitGadget
pack-bitmap.c +3 -0
+			table_size = st_add(table_size,
+					st_mult(ntohl(header->entry_count),
+						triplet_sz));

Here, we _do_ want to keep the st_mult(). Is the st_add() still necessary? It seems this is a leftover from the previous version that had the 4-byte flag data.

Quoted 5 lines

We set table_size to zero above. We could drop that initialization and instead have this after the "size_t triplet_sz" definition:

			size_t table_size = st_mult(ntohl(header->entry_count),
						    triplet_sz));

Well put, thank you.

Quoted 4 lines

pack-bitmap.c static int load_bitmap_header(struct bitmap_index *index) · line 206

Quoted 2 lines
pack-bitmap.c +2 -0
+			if (table_size > index_end - index->map - header_size)
+				return error("corrupted bitmap index file (too short to fit lookup table)");

Please add "_(...)" around the error message so it can be translated.

I missed this in my own review, but yes: this is a good practice.

Quoted 9 lines

pack-bitmap.c struct include_data { · line 634

Quoted 6 lines
pack-bitmap.c +6 -0
+	if (bitmap_git->midx)
+		found = bsearch_midx(oid, bitmap_git->midx, result);
+	else
+		found = bsearch_pack(oid, bitmap_git->pack, result);
+
+	return found;

Here, we are doing a binary search on the entire list of packed objects, which could use quite a few more hops than a binary search on the bitmapped commits.

I think this is the best we can do if we make the key to our bsearch through the lookup table be an index into the pack index / MIDX. But...

Quoted 14 lines
Quoted 2 lines
pack-bitmap.c +2 -0
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+					  struct commit *commit)

...

pack-bitmap.c struct include_data { · line 655

Quoted 7 lines
pack-bitmap.c +7 -0
+	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
+
+	if (!found)
+		return NULL;
+
+	triplet = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+						triplet_sz, triplet_cmp);

But I see, you are searching the pack-index for the position in the index, and _then_ searching the bitmap lookup table based on that position value.

Quoted 7 lines

I expected something different: binary search on the triplets where the comparison is made by looking up the OID from the [multi-]pack-index and comparing that OID to the commit OID we are looking for.

I'm not convinced that the binary search I had in mind is meaningfully faster than what you've implemented here, so I'm happy to leave it as you have it. We can investigate if that full search on the pack-index matters at all (it probably doesn't).

...exactly my thoughts, too. It's possible that it would be faster to key this search on the object_id "oid" above, and then convert each of the entries in the lookup table from a uint32_t into an object_id by calling nth_bitmap_object_oid() repeatedly.

I *think* that what Abhradeep wrote here is going to be faster more often than not since it makes more efficient use of the page cache rather than switching between reads across different memory mapped regions at each point in the binary search.

But of course that depends on a number of factors. Abhradeep: if you're up for it, I think it would be worth trying it both ways and seeing if one produces a meaningful speed-up or slow-down over the other. Like I
said: my guess is that what you have now will be faster, but I don't have a clear sense that that is true without trying it both ways ;-).

Quoted 10 lines
Quoted 10 lines
pack-bitmap.c +10 -0
+	if (!triplet)
+		return NULL;
+
+	offset = triplet_get_offset(triplet);
+	xor_pos = triplet_get_xor_pos(triplet);
+
+	if (xor_pos != 0xffffffff) {
+		int xor_flags;
+		uint64_t offset_xor;
+		uint32_t *xor_positions;
Quoted 8 lines

pack-bitmap.c struct include_data { · line 669

Quoted 4 lines
pack-bitmap.c +4 -0
+		struct object_id xor_oid;
+		size_t size = 0;
+
+		ALLOC_ARRAY(xor_positions, bitmap_git->entry_count);

While there is potential that this is wasteful, it's probably not that huge, so we can start with the "maximum XOR depth" and then reconsider a smaller allocation in the future.

There is no maximum XOR depth, to my knowledge. We do have a maximum XOR *offset*, which says we cannot XOR-compress a bitmap with an entry more than 160 entries away from the current one. But in theory every commit could be XOR compressed with the one immediately proceeding it, so the maximum depth could be as long as the entry_count itself.

I think starting off with a small array and then letting it grow according to alloc_nr() would be fine here, since it will grow more and more each time, so the amount of times we have to reallocate the buffer will tail off over time.

If we were really concerned about it, we could treat the buffer as a static pointer and reuse it over time (making sure to clear out the portions of it we're going to reuse, or otherwise ensuring that we don't read old data). But I doubt it matters much either way in practice: the individual records are small (at just 4 bytes each) and entry_count is often less than 1,000, so I think this probably has a vanishingly small impact.

Thanks,
Taylor

#68 lore raw

Re: [PATCH v2 6/6] p5310-pack-bitmaps.sh: enable pack.writeReverseIndex for testing

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 5 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Enable pack.writeReverseIndex to true to see the effect of writing the reverse index in the existing bitmap tests (with and without lookup table).

I think we should swap the order of these final two patches, since we're primarily interested in the difference between using a reverse index with and without the lookup table.

Thanks,
Taylor

#64 lore raw

Re: [PATCH v2 5/6] bitmap-lookup-table: add performance tests for lookup table

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 88 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add performance tests to verify the performance of lookup table.

Lookup table makes Git run faster in most of the cases. Below is the result of t/perf/p5310-pack-bitmaps.sh.perf/p5326-multi-pack-bitmaps.sh gives similar result. The repository used in the test is linux kernel.

Test this tree
--------------------------------------------------------------------------

5310.4: repack to disk (lookup=false)                   295.94(250.45+15.24)
5310.5: simulated clone                                 12.52(5.07+1.40)
5310.6: simulated fetch                                 1.89(2.94+0.24)
5310.7: pack to file (bitmap)                           41.39(20.33+7.20)
5310.8: rev-list (commits)                              0.98(0.59+0.12)
5310.9: rev-list (objects)                              3.40(3.27+0.10)
5310.10: rev-list with tag negated via --not		0.07(0.02+0.04)
         --all (objects)
5310.11: rev-list with negative tag (objects)           0.23(0.16+0.06)
5310.12: rev-list count with blob:none                  0.26(0.18+0.07)
5310.13: rev-list count with blob:limit=1k              6.45(5.94+0.37)
5310.14: rev-list count with tree:0                     0.26(0.18+0.07)
5310.15: simulated partial clone                        4.99(3.19+0.45)
5310.19: repack to disk (lookup=true)                   269.67(174.70+21.33)
5310.20: simulated clone                                11.03(5.07+1.11)
5310.21: simulated fetch                                0.79(0.79+0.17)
5310.22: pack to file (bitmap)                          43.03(20.28+7.43)
5310.23: rev-list (commits)                             0.86(0.54+0.09)
5310.24: rev-list (objects)                             3.35(3.26+0.07)
5310.25: rev-list with tag negated via --not		0.05(0.00+0.03)
	 --all (objects)
5310.26: rev-list with negative tag (objects)           0.22(0.16+0.05)
5310.27: rev-list count with blob:none                  0.22(0.16+0.05)
5310.28: rev-list count with blob:limit=1k              6.45(5.87+0.31)
5310.29: rev-list count with tree:0                     0.22(0.16+0.05)
5310.30: simulated partial clone                        5.17(3.12+0.48)

Test 4-15 are tested without using lookup table. Same tests are repeated in 16-30 (using lookup table).

---
 t/perf/p5310-pack-bitmaps.sh       | 77 ++++++++++++++-----------
 t/perf/p5326-multi-pack-bitmaps.sh | 93 ++++++++++++++++--------------
 2 files changed, 94 insertions(+), 76 deletions(-)
t/perf/p5310-pack-bitmaps.sh +0 -32
index 7ad4f237bc3..6ff42bdd391 100755
@@ -16,39 +16,48 @@ test_expect_success 'setup bitmap config' '
 	git config pack.writebitmaps true
 '
 
-# we need to create the tag up front such that it is covered by the repack and
-# thus by generated bitmaps.
-test_expect_success 'create tags' '
-	git tag --message="tag pointing to HEAD" perf-tag HEAD
-'
-
-test_perf 'repack to disk' '
-	git repack -ad
-'
-
-test_full_bitmap
-
-test_expect_success 'create partial bitmap state' '
-	# pick a commit to represent the repo tip in the past
-	cutoff=$(git rev-list HEAD~100 -1) &&
-	orig_tip=$(git rev-parse HEAD) &&
-
-	# now kill off all of the refs and pretend we had
-	# just the one tip
-	rm -rf .git/logs .git/refs/* .git/packed-refs &&
-	git update-ref HEAD $cutoff &&
-
-	# and then repack, which will leave us with a nice
-	# big bitmap pack of the "old" history, and all of
-	# the new history will be loose, as if it had been pushed
-	# up incrementally and exploded via unpack-objects
-	git repack -Ad &&
-
-	# and now restore our original tip, as if the pushes
-	# had happened
-	git update-ref HEAD $orig_tip
-'

t/perf/p5310-pack-bitmaps.sh test_expect_success 'setup bitmap config' ' · line 26

Quoted 10 lines
t/perf/p5310-pack-bitmaps.sh +8 -2
-
-test_partial_bitmap
+test_bitmap () {
+    local enabled="$1"
+
+	# we need to create the tag up front such that it is covered by the repack and
+	# thus by generated bitmaps.
+	test_expect_success 'create tags' '
+		git tag --message="tag pointing to HEAD" perf-tag HEAD
+	'

I think this "create tags" step can happen outside of the test_bitmap() function, since it should only need to be done once, right?

t/perf/p5310-pack-bitmaps.sh test_expect_success 'setup bitmap config' ' · line 34

Quoted 7 lines
t/perf/p5310-pack-bitmaps.sh +7 -0
+	test_expect_success "use lookup table: $enabled" '
+		git config pack.writeBitmapLookupTable '"$enabled"'
+	'
+
+	test_perf "repack to disk (lookup=$enabled)" '
+		git repack -ad
+	'

And I think these two tests could be combined, since this could just become:

git -c pack.writeBitmapLookupTable "$enabled" repack -ad

right?

t/perf/p5310-pack-bitmaps.sh test_expect_success 'setup bitmap config' ' · line 38

Quoted 3 lines
t/perf/p5310-pack-bitmaps.sh +3 -0
+	test_full_bitmap
+
+    test_expect_success "create partial bitmap state (lookup=$enabled)" '

There is some funky spacing going on here, at least in my email client. Could you double check that tabs are used consistently here?

Thanks,
Taylor

#65 lore raw

Re: [PATCH v2 5/6] bitmap-lookup-table: add performance tests for lookup table

Quoted 2 lines · Taylor Blau

I think this "create tags" step can happen outside of the test_bitmap() function, since it should only need to be done once, right?

Yeah, I also think the same. That's why I tried to not include in the Function but for some reason, one test is failing -

perf 24 - rev-list with tag negated via --not --all (objects):
running:
git rev-list perf-tag --not --all --use-bitmap-index --objects >/dev/null

fatal: ambiguous argument 'perf-tag': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
not ok 24 - rev-list with tag negated via --not --all (objects)

One thing to note here is that the first test_bitmap call always Passes. But the second test_bitmap call fails due to above error. It throws error irrespective of any parameters for second test_bitmap.

If I put it inside the function it doesn't throw any error!

For this reason, I put it into the function. Do you have any idea why this happend?

Quoted 6 lines

And I think these two tests could be combined, since this could just become:

git -c pack.writeBitmapLookupTable "$enabled" repack -ad

right?

Yeah, sure.

Quoted 2 lines

There is some funky spacing going on here, at least in my email client. Could you double check that tabs are used consistently here?

This is due to my editor's spacing issues. All seems fine when I look at it in my editor. But actually it is not. Fixing it.

Thanks :)

#69 lore raw

Re: [PATCH v2 6/6] p5310-pack-bitmaps.sh: enable pack.writeReverseIndex for testing

In reply to Taylor Blau
Quoted 3 lines · Taylor Blau

I think we should swap the order of these final two patches, since we're primarily interested in the difference between using a reverse index with and without the lookup table.

Ok. Thanks :)

#55 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table

In reply to Taylor Blau
Quoted 10 lines · Taylor Blau

...exactly my thoughts, too. It's possible that it would be faster to key this search on the object_id "oid" above, and then convert each of the entries in the lookup table from a uint32_t into an object_id by calling nth_bitmap_object_oid() repeatedly.

I *think* that what Abhradeep wrote here is going to be faster more often than not since it makes more efficient use of the page cache rather than switching between reads across different memory mapped regions at each point in the binary search.

Quoted 5 lines

But of course that depends on a number of factors. Abhradeep: if you're up for it, I think it would be worth trying it both ways and seeing if one produces a meaningful speed-up or slow-down over the other. Like I
said: my guess is that what you have now will be faster, but I don't have a clear sense that that is true without trying it both ways ;-).

Ok. Let me try both the ways. In my opinion, I think my version has less searching and less computation. So, I want to stick with this version. But I also like to try the other one once so that we can get the best out of these two.

Quoted 4 lines

I think starting off with a small array and then letting it grow according to alloc_nr() would be fine here, since it will grow more and more each time, so the amount of times we have to reallocate the buffer will tail off over time.

What should be the size of that array?

Quoted 7 lines

If we were really concerned about it, we could treat the buffer as a static pointer and reuse it over time (making sure to clear out the portions of it we're going to reuse, or otherwise ensuring that we don't read old data). But I doubt it matters much either way in practice: the individual records are small (at just 4 bytes each) and entry_count is often less than 1,000, so I think this probably has a vanishingly small impact.

Before submitting it to the mailing list, I did use the ALLOC_GROW macro function. But my version was worse than yours. For every iteration I was reallocating the array to support size+1 positions. But later I drop the code as this might be very much expensive.

Then I wrote this code. As table array and table_inv array allocate this size of arrays (though all the indices are used), I thought it would not be a problem if I use an array of this size for a small amount of time.

Honestly, I don't like to realloc arrays. Because as far as I can remember, realloc allocates a new array internally and copies the items from the old array to the new array. This irritates me.

But at the same time, it is also true that in most cases we might not need this amount of space.

Thanks :)

#59 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau

Ohh, sorry! Looks like I missed this comment!

Quoted 2 lines · Taylor Blau

It may be worth replacing "within map" to "within the memory mapped region map" to make clear that this points somewhere within the mmap.

Ok.

Quoted 11 lines

I should have commented on this in an earlier round, but I wonder what the behavior should be when we have BITMAP_OPT_LOOKUP_TABLE in our flags, but GIT_TEST_READ_COMMIT_TABLE is disabled.

Right now, it doesn't matter, since there aren't any flags in bits above BITMAP_OPT_LOOKUP_TABLE. But in the future, if there was some BITMAP_OPT_FOO that was newer than BITMAP_OPT_LOOKUP_TABLE, we would want to be able to read it without needing to read the lookup table.

At least, I think that should be true, though I would be interested to hear if anybody has a differing opinion there.

Oh right! I didn't think about it. In that case, we should still subtract The table size from the last index_size. In that way, These sections will Not be overlapped.

Quoted 14 lines

And table_size here is going to start off at zero, so the outer st_add() call isn't necessary, either. This should instead be:

    size_t table_size = st_mult(ntohl(header->entry_count),
                                sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uint32_t));

It might be nice to have triplet_sz #define'd somewhere else, since there are a handful of declarations in this patch that are all identical. Probably something like:

    #define BITMAP_LOOKUP_TABLE_RECORD_WIDTH (sizeof(uint32_t) + sizeof(uint64_t) + sizeof(uin32_t))

or even:

Quoted 9 lines
    /*
     * The width in bytes of a single record in the lookup table
     * extension:
     *
     *   (commit_pos, offset, xor_pos)
     *
     * whose fields are 32-, 64-, and 32-bits wide, respectively.
     */
     #define BITMAP_LOOKUP_TABLE_RECORD_WIDTH (16)

Seems perfect to me.

Quoted 9 lines

if we decide to still recognize the lookup table extension without *reading* from it when GIT_TEST_READ_COMMIT_TABLE is unset, I think we should do something like:

    if (git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1))
        index->table_lookup = (void *)(index_end - table_size);
    index_end -= table_size;

...where the subtraction on index_end happens unconditionally.

Right. Thanks!

Quoted 10 lines

I wonder if we could get rid of these functions altogether and return a small structure like:

    struct bitmap_lookup_table_record {
        uint32_t commit_pos;
        uint64_t offset;
        uint32_t xor_pos;
    };

or similar.

Ok.

Quoted 4 lines

Hmm. This is a little tricky to read. Here we're expecting "va" to hold commit_pos from below, and "vb" to be a pointer at a lookup record. Everything here is right, though I wonder if a comment or two might clarify why one is "*(uint32_t *)va" and the other is "get_be32(vb)".

Sure. Will add comments.

Quoted 2 lines

Nit: let's use the bitmap_is_midx() helper here instead of looking at bitamp_git->midx directly.

Ok.

Quoted 10 lines

First thing is to convert the commit OID we're looking for into its position within the corresponding pack index or MIDX file so that we can use it as a search key to locate in the lookup table. If we didn't find anything, or the commit doesn't exist in our pack / MIDX, nothing to do.

pack-bitmap.c struct include_data { · line 660

Quoted 3 lines
pack-bitmap.c +3 -0
+
+	offset = triplet_get_offset(triplet);
+	xor_pos = triplet_get_xor_pos(triplet);

Otherwise, record its offset and XOR "offset".

Exactly!

Quoted 4 lines

We already have to get the triplets in the loop above, and then we dig them back out here. Would it be easier to keep track of a list of pointers into the mmaped region instead of looking up these triplets each time?

Sure. It might be a good idea. Thanks.

Quoted 7 lines

pack-bitmap.c struct include_data { · line 682

Quoted 4 lines
pack-bitmap.c +4 -0
+			commit_pos = get_be32(triplet);
+			offset_xor = triplet_get_offset(triplet);
+
+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, commit_pos) < 0) {

Should it be an error if we can't look up the object's ID here? I'd think so.

I also am not sure about it. Morally, I think it is better to throw An error here.

Quoted 3 lines

Do we have a good way to make sure that we're testing this code in CI? It *seems* correct to me, but of course, we should have a computer check that this produces OK results, not a human ;).

My current test file changes should test this code. As for now, the lookup Table is enabled by default, all the existing tests that include write and read bitmaps uses this lookup table. So, all the test case scenarios should Pass. So, I think it is being tested in CI. Do you have a good idea to test It better?

Quoted 6 lines

Hmm. I'm not sure I follow the purpose of tweaking GIT_TEST_READ_COMMIT_TABLE like this with setenv(). Are we trying to avoid reading the lookup table? If so, why? I'd rather avoid manipulating the environment directly like this, and instead have a function we could call to fault in all of the bitmaps (when a lookup table exists, otherwise do nothing).

The problem was that the test-tool bitmap list-commit command was Not printing any commits (the error that I notified you before). It is because of this function. As lookup table is enabled by default, prepare_bitmap_git function doesn't load each bitmap entries and thus the below code in this function doesn't provide the bitmapped commit list (because Hashtable didn't generated).

        kh_foreach(bitmap_git->bitmaps, oid, value, {
		printf("%s\n", oid_to_hex(&oid));
	});

So, the simplest fix I found was this. Should I make a function then (Which you suggested here)?

Thanks :)

#49 lore raw

Re: [PATCH v2 3/6] pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Abhradeep Chakraborty
Quoted 11 lines · Abhradeep Chakraborty
Quoted 3 lines · Taylor Blau

Probably both of them should take into account their separate configuration values, but cleaning up the hashcache one can be done separately outside of this series.

Actually, it does respect the pack.writebitmaplookuptable config. As pack.writebitmaplookuptable is by default true (for this patch Series), this line enables it by default. If pack.writebitmaplookuptable Set to false, the proposed change in the git_multi_pack_index_write_config function disables this flag.

Aha, you're absolutely right. I missed the earlier hunk. Thanks for pointing it out, this part looks fine to me.

Thanks,
Taylor

#56 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table

In reply to Abhradeep Chakraborty
Quoted 18 lines · Abhradeep Chakraborty
Quoted 10 lines · Taylor Blau

...exactly my thoughts, too. It's possible that it would be faster to key this search on the object_id "oid" above, and then convert each of the entries in the lookup table from a uint32_t into an object_id by calling nth_bitmap_object_oid() repeatedly.

I *think* that what Abhradeep wrote here is going to be faster more often than not since it makes more efficient use of the page cache rather than switching between reads across different memory mapped regions at each point in the binary search.

Quoted 5 lines

But of course that depends on a number of factors. Abhradeep: if you're up for it, I think it would be worth trying it both ways and seeing if one produces a meaningful speed-up or slow-down over the other. Like I
said: my guess is that what you have now will be faster, but I don't have a clear sense that that is true without trying it both ways ;-).

Quoted 4 lines

Ok. Let me try both the ways. In my opinion, I think my version has less searching and less computation. So, I want to stick with this version. But I also like to try the other one once so that we can get the best out of these two.

Yeah, I agree with your general sense that the version as written is going to be faster. We're comparing a smaller datatype (IOW, a 4-byte integer that can be checked for equality in a single instruction, instead of comparing two 20-byte OIDs), and likely flushing the cache far less often.

But having two concrete implementations to compare will help us know for a fact that our intuition is correct.

I'll be curious to see what you find here!

Quoted 6 lines
Quoted 4 lines

I think starting off with a small array and then letting it grow according to alloc_nr() would be fine here, since it will grow more and more each time, so the amount of times we have to reallocate the buffer will tail off over time.

What should be the size of that array?

I think some small, power of 2 would be a reasonable choice here.

Quoted 8 lines
Quoted 7 lines

If we were really concerned about it, we could treat the buffer as a static pointer and reuse it over time (making sure to clear out the portions of it we're going to reuse, or otherwise ensuring that we don't read old data). But I doubt it matters much either way in practice: the individual records are small (at just 4 bytes each) and entry_count is often less than 1,000, so I think this probably has a vanishingly small impact.

Quoted 4 lines

Before submitting it to the mailing list, I did use the ALLOC_GROW macro function. But my version was worse than yours. For every iteration I was reallocating the array to support size+1 positions. But later I drop the code as this might be very much expensive.

That shouldn't be the case. When you have a chance, take a look at the alloc_nr macro, which shows how much memory we allocate at each step:

    #define alloc_nr(x) (((x)+16)*3/2)

Suppose we allocated 16 slots initially, so nr (the number of entries stored in the list) is 0 and alloc (the number of entries allocated) is
16. Then when we try to add the 17th item, we'll pass 16 to alloc_nr which will allocate 48 slots. Then 96, then 168, and so on.

We only have to reallocate and copy the array when nr > alloc, which should be fairly infrequently, and happens less and less often the larger the array grows.

Thanks,
Taylor

#60 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty
Quoted 10 lines · Abhradeep Chakraborty
Quoted 7 lines

Review

Quoted 4 lines
(quoted diff) +4 -0
+			commit_pos = get_be32(triplet);
+			offset_xor = triplet_get_offset(triplet);
+
+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, commit_pos) < 0) {

Should it be an error if we can't look up the object's ID here? I'd think so.

I also am not sure about it. Morally, I think it is better to throw An error here.

Yeah.

Quoted 9 lines
Quoted 3 lines

Do we have a good way to make sure that we're testing this code in CI? It *seems* correct to me, but of course, we should have a computer check that this produces OK results, not a human ;).

My current test file changes should test this code. As for now, the lookup Table is enabled by default, all the existing tests that include write and read bitmaps uses this lookup table. So, all the test case scenarios should Pass. So, I think it is being tested in CI. Do you have a good idea to test It better?

I think having some indication (maybe via a trace2 region?) that we're actually executing this code would be good. Although it's going to be *really* noisy, so probably not a good idea to do that in general.

Stolee runs some coverage tests that show lines that we aren't exercising via tests. So making sure that this doesn't show up in that report when you run it locally would be good.

See some information from him about how to run those tests locally here:

    https://lore.kernel.org/git/[email protected]/

(TL;DR: run make coverage-test and make sure that these lines don't show up ;-)).

Quoted 14 lines
Quoted 6 lines

Hmm. I'm not sure I follow the purpose of tweaking GIT_TEST_READ_COMMIT_TABLE like this with setenv(). Are we trying to avoid reading the lookup table? If so, why? I'd rather avoid manipulating the environment directly like this, and instead have a function we could call to fault in all of the bitmaps (when a lookup table exists, otherwise do nothing).

The problem was that the test-tool bitmap list-commit command was Not printing any commits (the error that I notified you before). It is because of this function. As lookup table is enabled by default, prepare_bitmap_git function doesn't load each bitmap entries and thus the below code in this function doesn't provide the bitmapped commit list (because Hashtable didn't generated).

Quoted 6 lines
        kh_foreach(bitmap_git->bitmaps, oid, value, {
		printf("%s\n", oid_to_hex(&oid));
	});

So, the simplest fix I found was this. Should I make a function then (Which you suggested here)?

I see. I remember that issue, but I think we should go about fixing it in a different way. Instead of tricking the code into loading all bitmaps by pretending the lookup table doesn't exist, we should have a function that forces loading in all bitmaps from the lookup table, if one exists. If the lookup table doesn't exist, or we have already loaded its entries, then that function can be noop.

If we had something like that, we could call that function from within test_bitmap_commits() before reading the keys and values out of bitmap_git->bitmaps.

An alternative approach would be to read the table directly when it exists, perhaps something like this:

--- 8< ---

pack-bitmap.c +16 -10
index 9e09c5824f..3bda059b9f 100644
@@ -1921,22 +1921,28 @@ int test_bitmap_commits(struct repository *r)
 {
 	struct bitmap_index *bitmap_git = NULL;
 	struct object_id oid;
-	MAYBE_UNUSED void *value;
-
-	/* As this function is only used to print bitmap selected
-	 * commits, we don't have to read the commit table.
-	 */
-	setenv("GIT_TEST_READ_COMMIT_TABLE", "0", 1);
 
 	bitmap_git = prepare_bitmap_git(r);
 	if (!bitmap_git)
 		die("failed to load bitmap indexes");
 
-	kh_foreach(bitmap_git->bitmaps, oid, value, {
-		printf("%s\n", oid_to_hex(&oid));
-	});
+	if (bitmap_git->table_lookup) {
+		uint32_t i, commit_pos;
+		for (i = 0; i < bitmap_git->entry_count; i++) {
+			commit_pos = get_be32(bitmap_get_triplet(bitmap_git, i));
+			if (nth_bitmap_object_oid(bitmap_git, &oid,
+						  commit_pos) < 0)
+				return error("could not find commit at "
+					     "position %"PRIu32, commit_pos);
+			printf("%s\n", oid_to_hex(&oid));
+		}
+	} else {
+		MAYBE_UNUSED void *value;
+		kh_foreach(bitmap_git->bitmaps, oid, value, {
+			printf("%s\n", oid_to_hex(&oid));
+		});
+	}
 
-	setenv("GIT_TEST_READ_COMMIT_TABLE", "1", 1);
 	free_bitmap_index(bitmap_git);
 
 	return 0;

--- >8 ---

Thanks,
Taylor

#66 lore raw

Re: [PATCH v2 5/6] bitmap-lookup-table: add performance tests for lookup table

In reply to Abhradeep Chakraborty
Quoted 17 lines · Abhradeep Chakraborty
Quoted 2 lines · Taylor Blau

I think this "create tags" step can happen outside of the test_bitmap() function, since it should only need to be done once, right?

Yeah, I also think the same. That's why I tried to not include in the Function but for some reason, one test is failing -

perf 24 - rev-list with tag negated via --not --all (objects):
running:
git rev-list perf-tag --not --all --use-bitmap-index --objects >/dev/null

fatal: ambiguous argument 'perf-tag': unknown revision or path not in the working tree. Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
not ok 24 - rev-list with tag negated via --not --all (objects)

Quoted 8 lines

One thing to note here is that the first test_bitmap call always Passes. But the second test_bitmap call fails due to above error. It throws error irrespective of any parameters for second test_bitmap.

If I put it inside the function it doesn't throw any error!

For this reason, I put it into the function. Do you have any idea why this happend?

I think that it's because we delete all of the refs in the test that creates a partial bitmap state. So anything that relies on perf-tag existing after that test runs will definitely not work :).

My original suggestion was misguided there, unless we wanted to make the aforementioned test (the one that creates the partial bitmap state) restore the ref state after it finishes running, but I don't think that's worthwhile.

Thanks,
Taylor

#61 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau

pack-bitmap.c int test_bitmap_commits(struct repository *r) · line 1936

Quoted 2 lines · Taylor Blau
pack-bitmap.c +2 -0
+				return error("could not find commit at "
+					     "position %"PRIu32, commit_pos);

Oops. Pretend that I marked this string for translation ;-).

Thanks,
Taylor

#57 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau
Quoted 6 lines · Taylor Blau

That shouldn't be the case. When you have a chance, take a look at the alloc_nr macro, which shows how much memory we allocate at each step:

    #define alloc_nr(x) (((x)+16)*3/2)
Quoted 8 lines

Suppose we allocated 16 slots initially, so nr (the number of entries stored in the list) is 0 and alloc (the number of entries allocated) is
16. Then when we try to add the 17th item, we'll pass 16 to alloc_nr which will allocate 48 slots. Then 96, then 168, and so on.

We only have to reallocate and copy the array when nr > alloc, which should be fairly infrequently, and happens less and less often the larger the array grows.

Ohh, I misunderstood the ALLOC_GROW function. Thanks!

#62 lore raw

Re: [PATCH v2 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau
Quoted 8 lines · Taylor Blau

I think having some indication (maybe via a trace2 region?) that we're actually executing this code would be good. Although it's going to be *really* noisy, so probably not a good idea to do that in general.

Stolee runs some coverage tests that show lines that we aren't exercising via tests. So making sure that this doesn't show up in that report when you run it locally would be good.

Quoted 6 lines

See some information from him about how to run those tests locally here:

    https://lore.kernel.org/git/[email protected]/

(TL;DR: run make coverage-test and make sure that these lines don't show up ;-)).

Got it. Thanks.

Quoted 6 lines

If we had something like that, we could call that function from within test_bitmap_commits() before reading the keys and values out of bitmap_git->bitmaps.

An alternative approach would be to read the table directly when it exists, perhaps something like this:

I think we have a simpler fix than what you suggested here. What if We do it like this way -

    if (bitmap_git->table_lookup) {
	if (load_bitmap_entries_v1(bitmap_git) < 0)
	    die(_("failed to load bitmap indexes"));
    }

Is this okay for you?

#78 lore raw

PATCH v3 2/6 pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documented by an earlier change, but Git does not yet know how to write that extension.

Teach Git to write bitmap lookup table extension. The table contains the list of N <commit_pos, offset, xor_row>` triplets. These triplets are sorted according to their commit pos (ascending order). The meaning of each data in the i'th triplet is given below:

- commit_pos stores commit position (in the pack-index or midx). It is a 4 byte network byte order unsigned integer.

- offset is the position (in the bitmap file) from which that commit's bitmap can be read.

- xor_row is the position of the triplet in the lookup table whose bitmap is used to compress this bitmap, or 0xffffffff if no such bitmap exists.

---
 pack-bitmap-write.c | 112 ++++++++++++++++++++++++++++++++++++++++++--
 pack-bitmap.h       |   5 +-
 2 files changed, 112 insertions(+), 5 deletions(-)
pack-bitmap-write.c +109 -3
index c43375bd344..4a0edd746bc 100644
@@ -648,9 +648,17 @@ static const struct object_id *oid_access(size_t pos, const void *table)
 	return &index[pos]->oid;
 }
 
+static int commit_bitmap_writer_pos(struct object_id *oid,
+				    struct pack_idx_entry **index,
+				    uint32_t index_nr)
+{
+	return oid_pos(oid, index, index_nr, oid_access);
+}
+
 static void write_selected_commits_v1(struct hashfile *f,
 				      struct pack_idx_entry **index,
-				      uint32_t index_nr)
+				      uint32_t index_nr,
+				      off_t *offsets)
 {
 	int i;
 
@@ -658,11 +666,14 @@ static void write_selected_commits_v1(struct hashfile *f,
 		struct bitmapped_commit *stored = &writer.selected[i];
 
 		int commit_pos =
-			oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
+			commit_bitmap_writer_pos(&stored->commit->object.oid, index, index_nr);
 
 		if (commit_pos < 0)
 			BUG("trying to write commit not in index");
 
+		if (offsets)
+			offsets[i] = hashfile_total(f);
+
 		hashwrite_be32(f, commit_pos);
 		hashwrite_u8(f, stored->xor_offset);
 		hashwrite_u8(f, stored->flags);
@@ -671,6 +682,92 @@ static void write_selected_commits_v1(struct hashfile *f,
 	}
 }
 
+static int table_cmp(const void *_va, const void *_vb, void *_data)
+{
+	uint32_t *commit_positions = _data;
+	uint32_t a = commit_positions[*(uint32_t *)_va];
+	uint32_t b = commit_positions[*(uint32_t *)_vb];
+
+	if (a > b)
+		return 1;
+	else if (a < b)
+		return -1;
+
+	return 0;
+}
+
+static void write_lookup_table(struct hashfile *f,
+			       struct pack_idx_entry **index,
+			       uint32_t index_nr,
+			       off_t *offsets)
+{
+	uint32_t i;
+	uint32_t *table, *table_inv, *commit_positions;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+	ALLOC_ARRAY(commit_positions, writer.selected_nr);
+
+	/* store the index positions of the commits */
+	for (i = 0; i < writer.selected_nr; i++) {
+		int pos = commit_bitmap_writer_pos(&writer.selected[i].commit->object.oid,
+						   index, index_nr);
+		if (pos < 0)
+			BUG(_("trying to write commit not in index"));
+
+		commit_positions[i] = pos;
+	}
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+
+	/*
+	 * At the end of this sort table[j] = i means that the i'th
+	 * bitmap corresponds to j'th bitmapped commit in lex order of
+	 * OIDs.
+	 */
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);
+
+	/* table_inv helps us discover that relationship (i'th bitmap
+	 * to j'th commit by j = table_inv[i])
+	 */
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;
+
+	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		uint32_t xor_offset = selected->xor_offset;
+		uint32_t xor_row;
+
+		if (xor_offset) {
+			/*
+			 * xor_index stores the index (in the bitmap entries)
+			 * of the corresponding xor bitmap. But we need to convert
+			 * this index into lookup table's index. So, table_inv[xor_index]
+			 * gives us the index position w.r.t. the lookup table.
+			 *
+			 * If "k = table[i] - xor_offset" then the xor base is the k'th
+			 * bitmap. `table_inv[k]` gives us the position of that bitmap
+			 * in the lookup table.
+			 */
+			uint32_t xor_index = table[i] - xor_offset;
+			xor_row = table_inv[xor_index];
+		} else {
+			xor_row = 0xffffffff;
+		}
+
+		hashwrite_be32(f, commit_positions[table[i]]);
+		hashwrite_be64(f, (uint64_t)offsets[table[i]]);
+		hashwrite_be32(f, xor_row);
+	}
+	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
+
+	free(table);
+	free(table_inv);
+	free(commit_positions);
+}
+
 static void write_hash_cache(struct hashfile *f,
 			     struct pack_idx_entry **index,
 			     uint32_t index_nr)
@@ -695,6 +792,7 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 {
 	static uint16_t default_version = 1;
 	static uint16_t flags = BITMAP_OPT_FULL_DAG;
+	off_t *offsets = NULL;
 	struct strbuf tmp_file = STRBUF_INIT;
 	struct hashfile *f;
 
@@ -715,7 +813,14 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 	dump_bitmap(f, writer.trees);
 	dump_bitmap(f, writer.blobs);
 	dump_bitmap(f, writer.tags);
-	write_selected_commits_v1(f, index, index_nr);
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		CALLOC_ARRAY(offsets, index_nr);
+
+	write_selected_commits_v1(f, index, index_nr, offsets);
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		write_lookup_table(f, index, index_nr, offsets);
 
 	if (options & BITMAP_OPT_HASH_CACHE)
 		write_hash_cache(f, index, index_nr);
@@ -730,4 +835,5 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 		die_errno("unable to rename temporary bitmap file to '%s'", filename);
 
 	strbuf_release(&tmp_file);
+	free(offsets);
 }
pack-bitmap.h +3 -2
index 3d3ddd77345..67a9d0fc303 100644
@@ -24,8 +24,9 @@ struct bitmap_disk_header {
 #define NEEDS_BITMAP (1u<<22)
 
 enum pack_bitmap_opts {
-	BITMAP_OPT_FULL_DAG = 1,
-	BITMAP_OPT_HASH_CACHE = 4,
+	BITMAP_OPT_FULL_DAG = 0x1,
+	BITMAP_OPT_HASH_CACHE = 0x4,
+	BITMAP_OPT_LOOKUP_TABLE = 0x10,
 };
 
 enum pack_bitmap_flags {
Signature · 1 line
gitgitgadget
#71 lore raw

PATCH v3 1/6 Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, Git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx or pack, along with their offset and xor offset. This way git can load only the necessary bitmaps without loading the previous bitmaps.

Older versions of Git ignore the lookup table extension and don't throw any kind of warning or error while parsing the bitmap file.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

---
 Documentation/technical/bitmap-format.txt | 39 +++++++++++++++++++++++
 1 file changed, 39 insertions(+)
Documentation/technical/bitmap-format.txt +39 -0
index 04b3ec21785..c30dc177643 100644
@@ -67,6 +67,17 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` <commit_pos, offset, xor_row>
+			triplets. The format and meaning of the table is described
+			below.
++
+NOTE: Unlike the xor_offset used to compress an individual bitmap,
+`xor_row` stores an *absolute* index into the lookup table, not a location
+relative to the current entry.
+
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +216,31 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last `N * (4 + 8 + 4)`
+bytes (preceding the name-hash cache and trailing hash) of the `.bitmap`
+file contains a lookup table specifying the information needed to get
+the desired bitmap from the entries without parsing previous unnecessary
+bitmaps.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the table
+contains a list of `nr_entries` <commit_pos, offset, xor_row> triplets
+(sorted in the ascending order of `commit_pos`). The content of i'th
+triplet is -
+
+	* {empty}
+	commit_pos (4 byte integer, network byte order): ::
+	It stores the object position of a commit (in the midx or pack
+	index).
+
+	* {empty}
+	offset (8 byte integer, network byte order): ::
+	The offset from which that commit's bitmap can be read.
+
+	* {empty}
+	xor_row (4 byte integer, network byte order): ::
+	The position of the triplet whose bitmap is used to compress
+	this one, or `0xffffffff` if no such bitmap exists.
Signature · 1 line
gitgitgadget
#70 lore raw

v3 cover bitmap: integrate a lookup table extension to the bitmap format

In reply to Abhradeep Chakraborty via GitGitGadget

When parsing the .bitmap file, git loads all the bitmaps one by one even if some of the bitmaps are not necessary. We can remove this overhead by loading only the necessary bitmaps. A look up table extension can solve this issue.

Changes since v1:

This is the second version which addressed all (I think) the reviews. Please notify me if some reviews are not addressed :)

* The table size is decreased and the format has also changed. It now contains nr_entries triplets of size 4+8+4 bytes. Each triplet contains the following things - (1) 4 byte commit position (in the pack-index or midx) (2) 8 byte offset and (3) 4 byte xor triplet (i.e. with whose bitmap the current triplet's bitmap has to xor) position.
* Performance tests are splitted into two commits. First contains the actual performance tests and second enables the pack.writeReverseIndex (as suggested by Taylor).
* st_*() functions are used.
* commit order is changed according to Derrick's suggestion.
* Iterative approach is used instead of recursive approach to parse xor bitmaps. (As suggested by Derrick).
* Some minor bug fixes of previous version.

Initial version:

The proposed table has:

* a list of nr_entries object ids. These objects are commits that has bitmaps. Ids are stored in lexicographic order (for better searching).
* a list of <offset, xor-offset> pairs (4-byte integers, network-byte order). The i'th pair denotes the offset and xor-offset(respectively) of the bitmap of i'th commit in the previous list. These two informations are necessary because only in this way bitmaps can be found without parsing all the bitmap.
* a 4-byte integer for table specific flags (none exists currently).

Whenever git want to parse the bitmap for a specific commit, it will first refer to the table and will look for the offset and xor-offset for that commit. Git will then try to parse the bitmap located at the offset position. The xor-offset can be used to find the xor-bitmap for the bitmap(if any).

Abhradeep Chakraborty (6):
Documentation/technical: describe bitmap lookup table extension pack-bitmap-write.c: write lookup table extension
pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
pack-bitmap: prepare to read lookup table extension
bitmap-lookup-table: add performance tests for lookup table p5310-pack-bitmaps.sh: remove pack.writeReverseIndex

 Documentation/config/pack.txt             |   7 +
 Documentation/technical/bitmap-format.txt |  39 ++
 builtin/multi-pack-index.c                |   7 +
 builtin/pack-objects.c                    |   8 +
 midx.c                                    |   3 +
 midx.h                                    |   1 +
 pack-bitmap-write.c                       | 112 ++-
 pack-bitmap.c                             | 266 +++++++-
 pack-bitmap.h                             |  14 +-
 t/perf/p5310-pack-bitmaps.sh              |  77 ++-
 t/perf/p5326-multi-pack-bitmaps.sh        |  93 +--
 t/t5310-pack-bitmaps.sh                   | 786 ++++++++++++----------
 t/t5311-pack-bitmaps-shallow.sh           |  53 +-
 t/t5326-multi-pack-bitmaps.sh             | 421 +++++++-----
 t/t5327-multi-pack-bitmaps-rev.sh         |   9 +
 15 files changed, 1238 insertions(+), 658 deletions(-)

base-commit: 39c15e485575089eb77c769f6da02f98a55905e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1266%2FAbhra303%2Fbitmap-commit-table-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1266/Abhra303/bitmap-commit-table-v3
Pull-Request: https://github.com/gitgitgadget/git/pull/1266

Range-diff vs v2:

1: 4d11be66cfa ! 1: f72bf11e6ef Documentation/technical: describe bitmap lookup table extension
@@ Metadata
## Commit message ##
Documentation/technical: describe bitmap lookup table extension

- When reading bitmap file, git loads each and every bitmap one by one
+ When reading bitmap file, Git loads each and every bitmap one by one
even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx or pack, along with their offset and xor offset. This way git can
- load only the neccesary bitmaps without loading the previous bitmaps.
+ load only the necessary bitmaps without loading the previous bitmaps.

- The older version of Git ignores the lookup table extension and doesn't
+ Older versions of Git ignore the lookup table extension and don't
throw any kind of warning or error while parsing the bitmap file.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

- Co-Authored-by: Taylor Blau <[email protected]>

+ Co-Authored-by: Taylor Blau <[email protected]>

## Documentation/technical/bitmap-format.txt ##
@@ Documentation/technical/bitmap-format.txt: MIDXs, both the bit-cache and rev-cac
+ ** {empty}
+ BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+ If present, the end of the bitmap file contains a table
-+ containing a list of N <commit pos, offset, xor offset>
++ containing a list of N <commit_pos, offset, xor_row>
+ triplets. The format and meaning of the table is described
+ below.
++
-+NOTE: This xor_offset is different from the bitmap's xor_offset. -+Bitmap's xor_offset is relative i.e. it tells how many bitmaps we have -+to go back from the current bitmap. Lookup table's xor_offset tells the -+position of the triplet in the list whose bitmap the current commit's -+bitmap have to xor with.
++NOTE: Unlike the xor_offset used to compress an individual bitmap, ++xor_row stores an *absolute* index into the lookup table, not a location ++relative to the current entry.
+
4-byte entry count (network byte order)

@@ Documentation/technical/bitmap-format.txt: Note that this hashing scheme is tied
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last N * (4 + 8 + 4)
-+(preceding the name-hash cache and trailing hash) of the .bitmap file -+contains a lookup table specifying the information needed to get the -+desired bitmap from the entries without parsing previous unnecessary ++bytes (preceding the name-hash cache and trailing hash) of the .bitmap ++file contains a lookup table specifying the information needed to get ++the desired bitmap from the entries without parsing previous unnecessary
+bitmaps.
+
+For a .bitmap containing nr_entries reachability bitmaps, the table
-+contains a list of nr_entries <commit pos, offset, xor offset> triplets. -+The content of i'th triplet is -
++contains a list of nr_entries <commit_pos, offset, xor_row> triplets ++(sorted in the ascending order of commit_pos). The content of i'th ++triplet is -
+
+ * {empty}
-+ commit pos (4 byte integer, network byte order): ::
-+ It stores the object position of the commit (in the midx or pack index)
-+ to which the i'th bitmap in the bitmap entries belongs.
++ commit_pos (4 byte integer, network byte order): ::
++ It stores the object position of a commit (in the midx or pack
++ index).
+
+ * {empty}
+ offset (8 byte integer, network byte order): ::
+ The offset from which that commit's bitmap can be read.
+
+ * {empty}
-+ xor offset (4 byte integer, network byte order): ::
-+ It holds the position of the triplet with whose bitmap the
-+ current bitmap need to xor. If the current triplet's bitmap
-+ do not have any xor bitmap, it defaults to 0xffffffff.
++ xor_row (4 byte integer, network byte order): ::
++ The position of the triplet whose bitmap is used to compress
++ this one, or 0xffffffff if no such bitmap exists.
2: d118f1d45e6 ! 2: 5e9b985e39b pack-bitmap-write.c: write lookup table extension

     @@ Metadata
       ## Commit message ##
          pack-bitmap-write.c: write lookup table extension
      
     -    The bitmap lookup table extension was documentated by an earlier
     -    change, but Git does not yet knowhow to write that extension.
     +    The bitmap lookup table extension was documented by an earlier
     +    change, but Git does not yet know how to write that extension.
      
     -    Teach git to write bitmap lookup table extension. The table contains
     -    the list of `N` <commit pos, offset, xor offset>` triplets. These
     +    Teach Git to write bitmap lookup table extension. The table contains
     +    the list of `N` <commit_pos, offset, xor_row>` triplets. These
          triplets are sorted according to their commit pos (ascending order).
          The meaning of each data in the i'th triplet is given below:
      
     -      - Commit pos is the position of the commit in the pack-index
     -        (or midx) to which the i'th bitmap belongs. It is a 4 byte
     -        network byte order integer.
     +      - commit_pos stores commit position (in the pack-index or midx).
     +        It is a 4 byte network byte order unsigned integer.
      
     -      - offset is the position of the i'th bitmap.
     +      - offset is the position (in the bitmap file) from which that
     +        commit's bitmap can be read.
      
     -      - xor offset denotes the position of the triplet with whose
     -        bitmap the current triplet's bitmap need to xor with.
     +      - xor_row is the position of the triplet in the lookup table
     +        whose bitmap is used to compress this bitmap, or `0xffffffff`
     +        if no such bitmap exists.
      
     -    Co-authored-by: Taylor Blau <[email protected]>
          Mentored-by: Taylor Blau <[email protected]>
          Co-mentored-by: Kaartic Sivaraam <[email protected]>
     +    Co-authored-by: Taylor Blau <[email protected]>
          Signed-off-by: Abhradeep Chakraborty <[email protected]>
      
       ## pack-bitmap-write.c ##
      @@ pack-bitmap-write.c: static const struct object_id *oid_access(size_t pos, const void *table)
     + 	return &index[pos]->oid;
     + }
       
     ++static int commit_bitmap_writer_pos(struct object_id *oid,
     ++				    struct pack_idx_entry **index,
     ++				    uint32_t index_nr)
     ++{
     ++	return oid_pos(oid, index, index_nr, oid_access);
     ++}
     ++
       static void write_selected_commits_v1(struct hashfile *f,
       				      struct pack_idx_entry **index,
      -				      uint32_t index_nr)
      +				      uint32_t index_nr,
     -+				      uint64_t *offsets,
     -+				      uint32_t *commit_positions)
     ++				      off_t *offsets)
       {
       	int i;
       
      @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
     + 		struct bitmapped_commit *stored = &writer.selected[i];
     + 
     + 		int commit_pos =
     +-			oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
     ++			commit_bitmap_writer_pos(&stored->commit->object.oid, index, index_nr);
     + 
       		if (commit_pos < 0)
       			BUG("trying to write commit not in index");
       
      +		if (offsets)
      +			offsets[i] = hashfile_total(f);
     -+		if (commit_positions)
     -+			commit_positions[i] = commit_pos;
      +
       		hashwrite_be32(f, commit_pos);
       		hashwrite_u8(f, stored->xor_offset);
     @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
       	}
       }
       
     -+static int table_cmp(const void *_va, const void *_vb, void *commit_positions)
     ++static int table_cmp(const void *_va, const void *_vb, void *_data)
      +{
     -+	int8_t result = 0;
     -+	uint32_t *positions = (uint32_t *) commit_positions;
     -+	uint32_t a = positions[*(uint32_t *)_va];
     -+	uint32_t b = positions[*(uint32_t *)_vb];
     ++	uint32_t *commit_positions = _data;
     ++	uint32_t a = commit_positions[*(uint32_t *)_va];
     ++	uint32_t b = commit_positions[*(uint32_t *)_vb];
      +
      +	if (a > b)
     -+		result = 1;
     ++		return 1;
      +	else if (a < b)
     -+		result = -1;
     -+	else
     -+		result = 0;
     ++		return -1;
      +
     -+	return result;
     ++	return 0;
      +}
      +
      +static void write_lookup_table(struct hashfile *f,
     -+			       uint64_t *offsets,
     -+			       uint32_t *commit_positions)
     ++			       struct pack_idx_entry **index,
     ++			       uint32_t index_nr,
     ++			       off_t *offsets)
      +{
      +	uint32_t i;
     -+	uint32_t *table, *table_inv;
     ++	uint32_t *table, *table_inv, *commit_positions;
      +
      +	ALLOC_ARRAY(table, writer.selected_nr);
      +	ALLOC_ARRAY(table_inv, writer.selected_nr);
     ++	ALLOC_ARRAY(commit_positions, writer.selected_nr);
     ++
     ++	/* store the index positions of the commits */
     ++	for (i = 0; i < writer.selected_nr; i++) {
     ++		int pos = commit_bitmap_writer_pos(&writer.selected[i].commit->object.oid,
     ++						   index, index_nr);
     ++		if (pos < 0)
     ++			BUG(_("trying to write commit not in index"));
     ++
     ++		commit_positions[i] = pos;
     ++	}
      +
      +	for (i = 0; i < writer.selected_nr; i++)
      +		table[i] = i;
      +
     ++	/*
     ++	 * At the end of this sort table[j] = i means that the i'th
     ++	 * bitmap corresponds to j'th bitmapped commit in lex order of
     ++	 * OIDs.
     ++	 */
      +	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);
      +
     ++	/* table_inv helps us discover that relationship (i'th bitmap
     ++	 * to j'th commit by j = table_inv[i])
     ++	 */
      +	for (i = 0; i < writer.selected_nr; i++)
      +		table_inv[table[i]] = i;
      +
     ++	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
      +	for (i = 0; i < writer.selected_nr; i++) {
      +		struct bitmapped_commit *selected = &writer.selected[table[i]];
      +		uint32_t xor_offset = selected->xor_offset;
     ++		uint32_t xor_row;
     ++
     ++		if (xor_offset) {
     ++			/*
     ++			 * xor_index stores the index (in the bitmap entries)
     ++			 * of the corresponding xor bitmap. But we need to convert
     ++			 * this index into lookup table's index. So, table_inv[xor_index]
     ++			 * gives us the index position w.r.t. the lookup table.
     ++			 *
     ++			 * If "k = table[i] - xor_offset" then the xor base is the k'th
     ++			 * bitmap. `table_inv[k]` gives us the position of that bitmap
     ++			 * in the lookup table.
     ++			 */
     ++			uint32_t xor_index = table[i] - xor_offset;
     ++			xor_row = table_inv[xor_index];
     ++		} else {
     ++			xor_row = 0xffffffff;
     ++		}
      +
      +		hashwrite_be32(f, commit_positions[table[i]]);
     -+		hashwrite_be64(f, offsets[table[i]]);
     -+		hashwrite_be32(f, xor_offset ?
     -+				table_inv[table[i] - xor_offset]: 0xffffffff);
     ++		hashwrite_be64(f, (uint64_t)offsets[table[i]]);
     ++		hashwrite_be32(f, xor_row);
      +	}
     ++	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
      +
      +	free(table);
      +	free(table_inv);
     ++	free(commit_positions);
      +}
      +
       static void write_hash_cache(struct hashfile *f,
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       {
       	static uint16_t default_version = 1;
       	static uint16_t flags = BITMAP_OPT_FULL_DAG;
     -+	uint64_t *offsets = NULL;
     -+	uint32_t *commit_positions = NULL;
     ++	off_t *offsets = NULL;
       	struct strbuf tmp_file = STRBUF_INIT;
       	struct hashfile *f;
       
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       	dump_bitmap(f, writer.blobs);
       	dump_bitmap(f, writer.tags);
      -	write_selected_commits_v1(f, index, index_nr);
     - 
     -+	if (options & BITMAP_OPT_LOOKUP_TABLE) {
     ++
     ++	if (options & BITMAP_OPT_LOOKUP_TABLE)
      +		CALLOC_ARRAY(offsets, index_nr);
     -+		CALLOC_ARRAY(commit_positions, index_nr);
     -+	}
      +
     -+	write_selected_commits_v1(f, index, index_nr, offsets, commit_positions);
     ++	write_selected_commits_v1(f, index, index_nr, offsets);
      +
      +	if (options & BITMAP_OPT_LOOKUP_TABLE)
     -+		write_lookup_table(f, offsets, commit_positions);
     ++		write_lookup_table(f, index, index_nr, offsets);
     + 
       	if (options & BITMAP_OPT_HASH_CACHE)
       		write_hash_cache(f, index, index_nr);
     - 
      @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       		die_errno("unable to rename temporary bitmap file to '%s'", filename);
       
       	strbuf_release(&tmp_file);
      +	free(offsets);
     -+	free(commit_positions);
       }
      
       ## pack-bitmap.h ##
 3:  7786dc879f0 ! 3:  3dc40cc7f73 pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
     @@ Metadata
       ## Commit message ##
          pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
      
     -    Teach git to provide a way for users to enable/disable bitmap lookup
     +    Teach Git to provide a way for users to enable/disable bitmap lookup
          table extension by providing a config option named 'writeBitmapLookupTable'.
     -    Default is true.
     +    Default is false.
      
          Also add test to verify writting of lookup table.
      
     -    Co-Authored-by: Taylor Blau <[email protected]>
     -    Signed-off-by: Abhradeep Chakraborty <[email protected]>
          Mentored-by: Taylor Blau <[email protected]>
          Co-Mentored-by: Kaartic Sivaraam <[email protected]>
     +    Co-Authored-by: Taylor Blau <[email protected]>
     +    Signed-off-by: Abhradeep Chakraborty <[email protected]>
      
       ## Documentation/config/pack.txt ##
      @@ Documentation/config/pack.txt: When writing a multi-pack reachability bitmap, no new namehashes are
     @@ Documentation/config/pack.txt: When writing a multi-pack reachability bitmap, no
       permuted into their appropriate location when writing a new bitmap.
       
      +pack.writeBitmapLookupTable::
     -+	When true, git will include a "lookup table" section in the
     ++	When true, Git will include a "lookup table" section in the
      +	bitmap index (if one is written). This table is used to defer
      +	loading individual bitmaps as late as possible. This can be
     -+	beneficial in repositories which have relatively large bitmap
     -+	indexes. Defaults to true.
     ++	beneficial in repositories that have relatively large bitmap
     ++	indexes. Defaults to false.
      +
       pack.writeReverseIndex::
       	When true, git will write a corresponding .rev file (see:
     @@ builtin/multi-pack-index.c: static int git_multi_pack_index_write_config(const c
       	/*
       	 * We should never make a fall-back call to 'git_default_config', since
       	 * this was already called in 'cmd_multi_pack_index()'.
     -@@ builtin/multi-pack-index.c: static int cmd_multi_pack_index_write(int argc, const char **argv)
     - 	};
     - 
     - 	opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE;
     -+	opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
     - 
     - 	git_config(git_multi_pack_index_write_config, NULL);
     - 
      
       ## builtin/pack-objects.c ##
     -@@ builtin/pack-objects.c: static enum {
     - 	WRITE_BITMAP_QUIET,
     - 	WRITE_BITMAP_TRUE,
     - } write_bitmap_index;
     --static uint16_t write_bitmap_options = BITMAP_OPT_HASH_CACHE;
     -+static uint16_t write_bitmap_options = BITMAP_OPT_HASH_CACHE | BITMAP_OPT_LOOKUP_TABLE;
     - 
     - static int exclude_promisor_objects;
     - 
      @@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v, void *cb)
       		else
       			write_bitmap_options &= ~BITMAP_OPT_HASH_CACHE;
     @@ midx.h: struct multi_pack_index {
       const unsigned char *get_midx_checksum(struct multi_pack_index *m);
       void get_midx_filename(struct strbuf *out, const char *object_dir);
      
     - ## pack-bitmap-write.c ##
     -@@ pack-bitmap-write.c: static void write_lookup_table(struct hashfile *f,
     - 	for (i = 0; i < writer.selected_nr; i++)
     - 		table_inv[table[i]] = i;
     - 
     -+	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
     - 	for (i = 0; i < writer.selected_nr; i++) {
     - 		struct bitmapped_commit *selected = &writer.selected[table[i]];
     - 		uint32_t xor_offset = selected->xor_offset;
     -@@ pack-bitmap-write.c: static void write_lookup_table(struct hashfile *f,
     - 
     - 	free(table);
     - 	free(table_inv);
     -+	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
     + ## t/t5310-pack-bitmaps.sh ##
     +@@ t/t5310-pack-bitmaps.sh: has_any () {
     + 	grep -Ff "$1" "$2"
       }
       
     - static void write_hash_cache(struct hashfile *f,
     -
     - ## t/t5310-pack-bitmaps.sh ##
     -@@ t/t5310-pack-bitmaps.sh: test_expect_success 'full repack creates bitmaps' '
     - 	ls .git/objects/pack/ | grep bitmap >output &&
     - 	test_line_count = 1 output &&
     - 	grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace &&
     +-setup_bitmap_history
     +-
     +-test_expect_success 'setup writing bitmaps during repack' '
     +-	git config repack.writeBitmaps true
     +-'
     +-
     +-test_expect_success 'full repack creates bitmaps' '
     +-	GIT_TRACE2_EVENT="$(pwd)/trace" \
     ++test_bitmap_cases () {
     ++	writeLookupTable=false
     ++	for i in "$@"
     ++	do
     ++		case "$i" in
     ++		"pack.writeBitmapLookupTable") writeLookupTable=true;;
     ++		esac
     ++	done
     ++
     ++	test_expect_success 'setup test repository' '
     ++		rm -fr * .git &&
     ++		git init &&
     ++		git config pack.writeBitmapLookupTable '"$writeLookupTable"'
     ++	'
     ++	setup_bitmap_history
     ++
     ++	test_expect_success 'setup writing bitmaps during repack' '
     ++		git config repack.writeBitmaps true
     ++	'
     ++
     ++	test_expect_success 'full repack creates bitmaps' '
     ++		GIT_TRACE2_EVENT="$(pwd)/trace" \
     ++			git repack -ad &&
     ++		ls .git/objects/pack/ | grep bitmap >output &&
     ++		test_line_count = 1 output &&
     ++		grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace &&
     ++		grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace
     ++	'
     ++
     ++	basic_bitmap_tests
     ++
     ++	test_expect_success 'pack-objects respects --local (non-local loose)' '
     ++		git init --bare alt.git &&
     ++		echo $(pwd)/alt.git/objects >.git/objects/info/alternates &&
     ++		echo content1 >file1 &&
     ++		# non-local loose object which is not present in bitmapped pack
     ++		altblob=$(GIT_DIR=alt.git git hash-object -w file1) &&
     ++		# non-local loose object which is also present in bitmapped pack
     ++		git cat-file blob $blob | GIT_DIR=alt.git git hash-object -w --stdin &&
     ++		git add file1 &&
     ++		test_tick &&
     ++		git commit -m commit_file1 &&
     ++		echo HEAD | git pack-objects --local --stdout --revs >1.pack &&
     ++		git index-pack 1.pack &&
     ++		list_packed_objects 1.idx >1.objects &&
     ++		printf "%s\n" "$altblob" "$blob" >nonlocal-loose &&
     ++		! has_any nonlocal-loose 1.objects
     ++	'
     ++
     ++	test_expect_success 'pack-objects respects --honor-pack-keep (local non-bitmapped pack)' '
     ++		echo content2 >file2 &&
     ++		blob2=$(git hash-object -w file2) &&
     ++		git add file2 &&
     ++		test_tick &&
     ++		git commit -m commit_file2 &&
     ++		printf "%s\n" "$blob2" "$bitmaptip" >keepobjects &&
     ++		pack2=$(git pack-objects pack2 <keepobjects) &&
     ++		mv pack2-$pack2.* .git/objects/pack/ &&
     ++		>.git/objects/pack/pack2-$pack2.keep &&
     ++		rm $(objpath $blob2) &&
     ++		echo HEAD | git pack-objects --honor-pack-keep --stdout --revs >2a.pack &&
     ++		git index-pack 2a.pack &&
     ++		list_packed_objects 2a.idx >2a.objects &&
     ++		! has_any keepobjects 2a.objects
     ++	'
     ++
     ++	test_expect_success 'pack-objects respects --local (non-local pack)' '
     ++		mv .git/objects/pack/pack2-$pack2.* alt.git/objects/pack/ &&
     ++		echo HEAD | git pack-objects --local --stdout --revs >2b.pack &&
     ++		git index-pack 2b.pack &&
     ++		list_packed_objects 2b.idx >2b.objects &&
     ++		! has_any keepobjects 2b.objects
     ++	'
     ++
     ++	test_expect_success 'pack-objects respects --honor-pack-keep (local bitmapped pack)' '
     ++		ls .git/objects/pack/ | grep bitmap >output &&
     ++		test_line_count = 1 output &&
     ++		packbitmap=$(basename $(cat output) .bitmap) &&
     ++		list_packed_objects .git/objects/pack/$packbitmap.idx >packbitmap.objects &&
     ++		test_when_finished "rm -f .git/objects/pack/$packbitmap.keep" &&
     ++		>.git/objects/pack/$packbitmap.keep &&
     ++		echo HEAD | git pack-objects --honor-pack-keep --stdout --revs >3a.pack &&
     ++		git index-pack 3a.pack &&
     ++		list_packed_objects 3a.idx >3a.objects &&
     ++		! has_any packbitmap.objects 3a.objects
     ++	'
     ++
     ++	test_expect_success 'pack-objects respects --local (non-local bitmapped pack)' '
     ++		mv .git/objects/pack/$packbitmap.* alt.git/objects/pack/ &&
     ++		rm -f .git/objects/pack/multi-pack-index &&
     ++		test_when_finished "mv alt.git/objects/pack/$packbitmap.* .git/objects/pack/" &&
     ++		echo HEAD | git pack-objects --local --stdout --revs >3b.pack &&
     ++		git index-pack 3b.pack &&
     ++		list_packed_objects 3b.idx >3b.objects &&
     ++		! has_any packbitmap.objects 3b.objects
     ++	'
     ++
     ++	test_expect_success 'pack-objects to file can use bitmap' '
     ++		# make sure we still have 1 bitmap index from previous tests
     ++		ls .git/objects/pack/ | grep bitmap >output &&
     ++		test_line_count = 1 output &&
     ++		# verify equivalent packs are generated with/without using bitmap index
     ++		packasha1=$(git pack-objects --no-use-bitmap-index --all packa </dev/null) &&
     ++		packbsha1=$(git pack-objects --use-bitmap-index --all packb </dev/null) &&
     ++		list_packed_objects packa-$packasha1.idx >packa.objects &&
     ++		list_packed_objects packb-$packbsha1.idx >packb.objects &&
     ++		test_cmp packa.objects packb.objects
     ++	'
     ++
     ++	test_expect_success 'full repack, reusing previous bitmaps' '
     + 		git repack -ad &&
     +-	ls .git/objects/pack/ | grep bitmap >output &&
     +-	test_line_count = 1 output &&
     +-	grep "\"key\":\"num_selected_commits\",\"value\":\"106\"" trace &&
      -	grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace
     -+	grep "\"key\":\"num_maximal_commits\",\"value\":\"107\"" trace &&
     -+	grep "\"label\":\"writing_lookup_table\"" trace
     +-'
     ++		ls .git/objects/pack/ | grep bitmap >output &&
     ++		test_line_count = 1 output
     ++	'
     ++
     ++	test_expect_success 'fetch (full bitmap)' '
     ++		git --git-dir=clone.git fetch origin second:second &&
     ++		git rev-parse HEAD >expect &&
     ++		git --git-dir=clone.git rev-parse HEAD >actual &&
     ++		test_cmp expect actual
     ++	'
     ++
     ++	test_expect_success 'create objects for missing-HAVE tests' '
     ++		blob=$(echo "missing have" | git hash-object -w --stdin) &&
     ++		tree=$(printf "100644 blob $blob\tfile\n" | git mktree) &&
     ++		parent=$(echo parent | git commit-tree $tree) &&
     ++		commit=$(echo commit | git commit-tree $tree -p $parent) &&
     ++		cat >revs <<-EOF
     ++		HEAD
     ++		^HEAD^
     ++		^$commit
     ++		EOF
     ++	'
     ++
     ++	test_expect_success 'pack-objects respects --incremental' '
     ++		cat >revs2 <<-EOF &&
     ++		HEAD
     ++		$commit
     ++		EOF
     ++		git pack-objects --incremental --stdout --revs <revs2 >4.pack &&
     ++		git index-pack 4.pack &&
     ++		list_packed_objects 4.idx >4.objects &&
     ++		test_line_count = 4 4.objects &&
     ++		git rev-list --objects $commit >revlist &&
     ++		cut -d" " -f1 revlist |sort >objects &&
     ++		test_cmp 4.objects objects
     ++	'
     ++
     ++	test_expect_success 'pack with missing blob' '
     ++		rm $(objpath $blob) &&
     ++		git pack-objects --stdout --revs <revs >/dev/null
     ++	'
     ++
     ++	test_expect_success 'pack with missing tree' '
     ++		rm $(objpath $tree) &&
     ++		git pack-objects --stdout --revs <revs >/dev/null
     ++	'
     ++
     ++	test_expect_success 'pack with missing parent' '
     ++		rm $(objpath $parent) &&
     ++		git pack-objects --stdout --revs <revs >/dev/null
     ++	'
     ++
     ++	test_expect_success JGIT,SHA1 'we can read jgit bitmaps' '
     ++		git clone --bare . compat-jgit.git &&
     ++		(
     ++			cd compat-jgit.git &&
     ++			rm -f objects/pack/*.bitmap &&
     ++			jgit gc &&
     ++			git rev-list --test-bitmap HEAD
     ++		)
     ++	'
     ++
     ++	test_expect_success JGIT,SHA1 'jgit can read our bitmaps' '
     ++		git clone --bare . compat-us.git &&
     ++		(
     ++			cd compat-us.git &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     ++			git repack -adb &&
     ++			# jgit gc will barf if it does not like our bitmaps
     ++			jgit gc
     ++		)
     ++	'
     ++
     ++	test_expect_success 'splitting packs does not generate bogus bitmaps' '
     ++		test-tool genrandom foo $((1024 * 1024)) >rand &&
     ++		git add rand &&
     ++		git commit -m "commit with big file" &&
     ++		git -c pack.packSizeLimit=500k repack -adb &&
     ++		git init --bare no-bitmaps.git &&
     ++		git -C no-bitmaps.git fetch .. HEAD
     ++	'
     ++
     ++	test_expect_success 'set up reusable pack' '
     ++		rm -f .git/objects/pack/*.keep &&
     ++		git repack -adb &&
     ++		reusable_pack () {
     ++			git for-each-ref --format="%(objectname)" |
     ++			git pack-objects --delta-base-offset --revs --stdout "$@"
     ++		}
     ++	'
     ++
     ++	test_expect_success 'pack reuse respects --honor-pack-keep' '
     ++		test_when_finished "rm -f .git/objects/pack/*.keep" &&
     ++		for i in .git/objects/pack/*.pack
     ++		do
     ++			>${i%.pack}.keep || return 1
     ++		done &&
     ++		reusable_pack --honor-pack-keep >empty.pack &&
     ++		git index-pack empty.pack &&
     ++		git show-index <empty.idx >actual &&
     ++		test_must_be_empty actual
     ++	'
     ++
     ++	test_expect_success 'pack reuse respects --local' '
     ++		mv .git/objects/pack/* alt.git/objects/pack/ &&
     ++		test_when_finished "mv alt.git/objects/pack/* .git/objects/pack/" &&
     ++		reusable_pack --local >empty.pack &&
     ++		git index-pack empty.pack &&
     ++		git show-index <empty.idx >actual &&
     ++		test_must_be_empty actual
     ++	'
     ++
     ++	test_expect_success 'pack reuse respects --incremental' '
     ++		reusable_pack --incremental >empty.pack &&
     ++		git index-pack empty.pack &&
     ++		git show-index <empty.idx >actual &&
     ++		test_must_be_empty actual
     ++	'
     ++
     ++	test_expect_success 'truncated bitmap fails gracefully (ewah)' '
     ++		test_config pack.writebitmaphashcache false &&
     ++		git repack -ad &&
     ++		git rev-list --use-bitmap-index --count --all >expect &&
     ++		bitmap=$(ls .git/objects/pack/*.bitmap) &&
     ++		test_when_finished "rm -f $bitmap" &&
     ++		test_copy_bytes 256 <$bitmap >$bitmap.tmp &&
     ++		mv -f $bitmap.tmp $bitmap &&
     ++		git rev-list --use-bitmap-index --count --all >actual 2>stderr &&
     ++		test_cmp expect actual &&
     ++		test_i18ngrep corrupt.ewah.bitmap stderr
     ++	'
     ++
     ++	test_expect_success 'truncated bitmap fails gracefully (cache)' '
     ++		git repack -ad &&
     ++		git rev-list --use-bitmap-index --count --all >expect &&
     ++		bitmap=$(ls .git/objects/pack/*.bitmap) &&
     ++		test_when_finished "rm -f $bitmap" &&
     ++		test_copy_bytes 512 <$bitmap >$bitmap.tmp &&
     ++		mv -f $bitmap.tmp $bitmap &&
     ++		git rev-list --use-bitmap-index --count --all >actual 2>stderr &&
     ++		test_cmp expect actual &&
     ++		test_i18ngrep corrupted.bitmap.index stderr
     ++	'
     ++
     ++	# Create a state of history with these properties:
     ++	#
     ++	#  - refs that allow a client to fetch some new history, while sharing some old
     ++	#    history with the server; we use branches delta-reuse-old and
     ++	#    delta-reuse-new here
     ++	#
     ++	#  - the new history contains an object that is stored on the server as a delta
     ++	#    against a base that is in the old history
     ++	#
     ++	#  - the base object is not immediately reachable from the tip of the old
     ++	#    history; finding it would involve digging down through history we know the
     ++	#    other side has
     ++	#
     ++	# This should result in a state where fetching from old->new would not
     ++	# traditionally reuse the on-disk delta (because we'd have to dig to realize
     ++	# that the client has it), but we will do so if bitmaps can tell us cheaply
     ++	# that the other side has it.
     ++	test_expect_success 'set up thin delta-reuse parent' '
     ++		# This first commit contains the buried base object.
     ++		test-tool genrandom delta 16384 >file &&
     ++		git add file &&
     ++		git commit -m "delta base" &&
     ++		base=$(git rev-parse --verify HEAD:file) &&
     ++
     ++		# These intermediate commits bury the base back in history.
     ++		# This becomes the "old" state.
     ++		for i in 1 2 3 4 5
     ++		do
     ++			echo $i >file &&
     ++			git commit -am "intermediate $i" || return 1
     ++		done &&
     ++		git branch delta-reuse-old &&
     ++
     ++		# And now our new history has a delta against the buried base. Note
     ++		# that this must be smaller than the original file, since pack-objects
     ++		# prefers to create deltas from smaller objects to larger.
     ++		test-tool genrandom delta 16300 >file &&
     ++		git commit -am "delta result" &&
     ++		delta=$(git rev-parse --verify HEAD:file) &&
     ++		git branch delta-reuse-new &&
     ++
     ++		# Repack with bitmaps and double check that we have the expected delta
     ++		# relationship.
     ++		git repack -adb &&
     ++		have_delta $delta $base
     ++	'
     ++
     ++	# Now we can sanity-check the non-bitmap behavior (that the server is not able
     ++	# to reuse the delta). This isn't strictly something we care about, so this
     ++	# test could be scrapped in the future. But it makes sure that the next test is
     ++	# actually triggering the feature we want.
     ++	#
     ++	# Note that our tools for working with on-the-wire "thin" packs are limited. So
     ++	# we actually perform the fetch, retain the resulting pack, and inspect the
     ++	# result.
     ++	test_expect_success 'fetch without bitmaps ignores delta against old base' '
     ++		test_config pack.usebitmaps false &&
     ++		test_when_finished "rm -rf client.git" &&
     ++		git init --bare client.git &&
     ++		(
     ++			cd client.git &&
     ++			git config transfer.unpackLimit 1 &&
     ++			git fetch .. delta-reuse-old:delta-reuse-old &&
     ++			git fetch .. delta-reuse-new:delta-reuse-new &&
     ++			have_delta $delta $ZERO_OID
     ++		)
     ++	'
     ++
     ++	# And do the same for the bitmap case, where we do expect to find the delta.
     ++	test_expect_success 'fetch with bitmaps can reuse old base' '
     ++		test_config pack.usebitmaps true &&
     ++		test_when_finished "rm -rf client.git" &&
     ++		git init --bare client.git &&
     ++		(
     ++			cd client.git &&
     ++			git config transfer.unpackLimit 1 &&
     ++			git fetch .. delta-reuse-old:delta-reuse-old &&
     ++			git fetch .. delta-reuse-new:delta-reuse-new &&
     ++			have_delta $delta $base
     ++		)
     ++	'
     ++
     ++	test_expect_success 'pack.preferBitmapTips' '
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     ++
     ++			# create enough commits that not all are receive bitmap
     ++			# coverage even if they are all at the tip of some reference.
     ++			test_commit_bulk --message="%s" 103 &&
     ++
     ++			git rev-list HEAD >commits.raw &&
     ++			sort <commits.raw >commits &&
     ++
     ++			git log --format="create refs/tags/%s %H" HEAD >refs &&
     ++			git update-ref --stdin <refs &&
     ++
     ++			git repack -adb &&
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++
     ++			# remember which commits did not receive bitmaps
     ++			comm -13 bitmaps commits >before &&
     ++			test_file_not_empty before &&
     ++
     ++			# mark the commits which did not receive bitmaps as preferred,
     ++			# and generate the bitmap again
     ++			perl -pe "s{^}{create refs/tags/include/$. }" <before |
     ++				git update-ref --stdin &&
     ++			git -c pack.preferBitmapTips=refs/tags/include repack -adb &&
     ++
     ++			# finally, check that the commit(s) without bitmap coverage
     ++			# are not the same ones as before
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++			comm -13 bitmaps commits >after &&
     ++
     ++			! test_cmp before after
     ++		)
     ++	'
     ++
     ++	test_expect_success 'complains about multiple pack bitmaps' '
     ++		rm -fr repo &&
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     ++
     ++			test_commit base &&
     ++
     ++			git repack -adb &&
     ++			bitmap="$(ls .git/objects/pack/pack-*.bitmap)" &&
     ++			mv "$bitmap" "$bitmap.bak" &&
     ++
     ++			test_commit other &&
     ++			git repack -ab &&
     ++
     ++			mv "$bitmap.bak" "$bitmap" &&
     ++
     ++			find .git/objects/pack -type f -name "*.pack" >packs &&
     ++			find .git/objects/pack -type f -name "*.bitmap" >bitmaps &&
     ++			test_line_count = 2 packs &&
     ++			test_line_count = 2 bitmaps &&
     ++
     ++			git rev-list --use-bitmap-index HEAD 2>err &&
     ++			grep "ignoring extra bitmap file" err
     ++		)
     ++	'
     ++}
     + 
     +-basic_bitmap_tests
     ++test_bitmap_cases
     + 
     + test_expect_success 'incremental repack fails when bitmaps are requested' '
     + 	test_commit more-1 &&
     +@@ t/t5310-pack-bitmaps.sh: test_expect_success 'incremental repack can disable bitmaps' '
     + 	git repack -d --no-write-bitmap-index
     + '
     + 
     +-test_expect_success 'pack-objects respects --local (non-local loose)' '
     +-	git init --bare alt.git &&
     +-	echo $(pwd)/alt.git/objects >.git/objects/info/alternates &&
     +-	echo content1 >file1 &&
     +-	# non-local loose object which is not present in bitmapped pack
     +-	altblob=$(GIT_DIR=alt.git git hash-object -w file1) &&
     +-	# non-local loose object which is also present in bitmapped pack
     +-	git cat-file blob $blob | GIT_DIR=alt.git git hash-object -w --stdin &&
     +-	git add file1 &&
     +-	test_tick &&
     +-	git commit -m commit_file1 &&
     +-	echo HEAD | git pack-objects --local --stdout --revs >1.pack &&
     +-	git index-pack 1.pack &&
     +-	list_packed_objects 1.idx >1.objects &&
     +-	printf "%s\n" "$altblob" "$blob" >nonlocal-loose &&
     +-	! has_any nonlocal-loose 1.objects
     +-'
     +-
     +-test_expect_success 'pack-objects respects --honor-pack-keep (local non-bitmapped pack)' '
     +-	echo content2 >file2 &&
     +-	blob2=$(git hash-object -w file2) &&
     +-	git add file2 &&
     +-	test_tick &&
     +-	git commit -m commit_file2 &&
     +-	printf "%s\n" "$blob2" "$bitmaptip" >keepobjects &&
     +-	pack2=$(git pack-objects pack2 <keepobjects) &&
     +-	mv pack2-$pack2.* .git/objects/pack/ &&
     +-	>.git/objects/pack/pack2-$pack2.keep &&
     +-	rm $(objpath $blob2) &&
     +-	echo HEAD | git pack-objects --honor-pack-keep --stdout --revs >2a.pack &&
     +-	git index-pack 2a.pack &&
     +-	list_packed_objects 2a.idx >2a.objects &&
     +-	! has_any keepobjects 2a.objects
     +-'
     +-
     +-test_expect_success 'pack-objects respects --local (non-local pack)' '
     +-	mv .git/objects/pack/pack2-$pack2.* alt.git/objects/pack/ &&
     +-	echo HEAD | git pack-objects --local --stdout --revs >2b.pack &&
     +-	git index-pack 2b.pack &&
     +-	list_packed_objects 2b.idx >2b.objects &&
     +-	! has_any keepobjects 2b.objects
     +-'
     +-
     +-test_expect_success 'pack-objects respects --honor-pack-keep (local bitmapped pack)' '
     +-	ls .git/objects/pack/ | grep bitmap >output &&
     +-	test_line_count = 1 output &&
     +-	packbitmap=$(basename $(cat output) .bitmap) &&
     +-	list_packed_objects .git/objects/pack/$packbitmap.idx >packbitmap.objects &&
     +-	test_when_finished "rm -f .git/objects/pack/$packbitmap.keep" &&
     +-	>.git/objects/pack/$packbitmap.keep &&
     +-	echo HEAD | git pack-objects --honor-pack-keep --stdout --revs >3a.pack &&
     +-	git index-pack 3a.pack &&
     +-	list_packed_objects 3a.idx >3a.objects &&
     +-	! has_any packbitmap.objects 3a.objects
     +-'
     +-
     +-test_expect_success 'pack-objects respects --local (non-local bitmapped pack)' '
     +-	mv .git/objects/pack/$packbitmap.* alt.git/objects/pack/ &&
     +-	rm -f .git/objects/pack/multi-pack-index &&
     +-	test_when_finished "mv alt.git/objects/pack/$packbitmap.* .git/objects/pack/" &&
     +-	echo HEAD | git pack-objects --local --stdout --revs >3b.pack &&
     +-	git index-pack 3b.pack &&
     +-	list_packed_objects 3b.idx >3b.objects &&
     +-	! has_any packbitmap.objects 3b.objects
     +-'
     +-
     +-test_expect_success 'pack-objects to file can use bitmap' '
     +-	# make sure we still have 1 bitmap index from previous tests
     +-	ls .git/objects/pack/ | grep bitmap >output &&
     +-	test_line_count = 1 output &&
     +-	# verify equivalent packs are generated with/without using bitmap index
     +-	packasha1=$(git pack-objects --no-use-bitmap-index --all packa </dev/null) &&
     +-	packbsha1=$(git pack-objects --use-bitmap-index --all packb </dev/null) &&
     +-	list_packed_objects packa-$packasha1.idx >packa.objects &&
     +-	list_packed_objects packb-$packbsha1.idx >packb.objects &&
     +-	test_cmp packa.objects packb.objects
     +-'
     +-
     +-test_expect_success 'full repack, reusing previous bitmaps' '
     +-	git repack -ad &&
     +-	ls .git/objects/pack/ | grep bitmap >output &&
     +-	test_line_count = 1 output
     +-'
     +-
     +-test_expect_success 'fetch (full bitmap)' '
     +-	git --git-dir=clone.git fetch origin second:second &&
     +-	git rev-parse HEAD >expect &&
     +-	git --git-dir=clone.git rev-parse HEAD >actual &&
     +-	test_cmp expect actual
     +-'
     +-
     +-test_expect_success 'create objects for missing-HAVE tests' '
     +-	blob=$(echo "missing have" | git hash-object -w --stdin) &&
     +-	tree=$(printf "100644 blob $blob\tfile\n" | git mktree) &&
     +-	parent=$(echo parent | git commit-tree $tree) &&
     +-	commit=$(echo commit | git commit-tree $tree -p $parent) &&
     +-	cat >revs <<-EOF
     +-	HEAD
     +-	^HEAD^
     +-	^$commit
     +-	EOF
     +-'
     +-
     +-test_expect_success 'pack-objects respects --incremental' '
     +-	cat >revs2 <<-EOF &&
     +-	HEAD
     +-	$commit
     +-	EOF
     +-	git pack-objects --incremental --stdout --revs <revs2 >4.pack &&
     +-	git index-pack 4.pack &&
     +-	list_packed_objects 4.idx >4.objects &&
     +-	test_line_count = 4 4.objects &&
     +-	git rev-list --objects $commit >revlist &&
     +-	cut -d" " -f1 revlist |sort >objects &&
     +-	test_cmp 4.objects objects
     +-'
     +-
     +-test_expect_success 'pack with missing blob' '
     +-	rm $(objpath $blob) &&
     +-	git pack-objects --stdout --revs <revs >/dev/null
     +-'
     ++test_bitmap_cases "pack.writeBitmapLookupTable"
     + 
     +-test_expect_success 'pack with missing tree' '
     +-	rm $(objpath $tree) &&
     +-	git pack-objects --stdout --revs <revs >/dev/null
     +-'
     +-
     +-test_expect_success 'pack with missing parent' '
     +-	rm $(objpath $parent) &&
     +-	git pack-objects --stdout --revs <revs >/dev/null
     +-'
     +-
     +-test_expect_success JGIT,SHA1 'we can read jgit bitmaps' '
     +-	git clone --bare . compat-jgit.git &&
     +-	(
     +-		cd compat-jgit.git &&
     +-		rm -f objects/pack/*.bitmap &&
     +-		jgit gc &&
     +-		git rev-list --test-bitmap HEAD
     +-	)
     +-'
     +-
     +-test_expect_success JGIT,SHA1 'jgit can read our bitmaps' '
     +-	git clone --bare . compat-us.git &&
     +-	(
     +-		cd compat-us.git &&
     +-		git repack -adb &&
     +-		# jgit gc will barf if it does not like our bitmaps
     +-		jgit gc
     +-	)
     +-'
     +-
     +-test_expect_success 'splitting packs does not generate bogus bitmaps' '
     +-	test-tool genrandom foo $((1024 * 1024)) >rand &&
     +-	git add rand &&
     +-	git commit -m "commit with big file" &&
     +-	git -c pack.packSizeLimit=500k repack -adb &&
     +-	git init --bare no-bitmaps.git &&
     +-	git -C no-bitmaps.git fetch .. HEAD
     +-'
     +-
     +-test_expect_success 'set up reusable pack' '
     +-	rm -f .git/objects/pack/*.keep &&
     +-	git repack -adb &&
     +-	reusable_pack () {
     +-		git for-each-ref --format="%(objectname)" |
     +-		git pack-objects --delta-base-offset --revs --stdout "$@"
     +-	}
     +-'
     +-
     +-test_expect_success 'pack reuse respects --honor-pack-keep' '
     +-	test_when_finished "rm -f .git/objects/pack/*.keep" &&
     +-	for i in .git/objects/pack/*.pack
     +-	do
     +-		>${i%.pack}.keep || return 1
     +-	done &&
     +-	reusable_pack --honor-pack-keep >empty.pack &&
     +-	git index-pack empty.pack &&
     +-	git show-index <empty.idx >actual &&
     +-	test_must_be_empty actual
     +-'
     +-
     +-test_expect_success 'pack reuse respects --local' '
     +-	mv .git/objects/pack/* alt.git/objects/pack/ &&
     +-	test_when_finished "mv alt.git/objects/pack/* .git/objects/pack/" &&
     +-	reusable_pack --local >empty.pack &&
     +-	git index-pack empty.pack &&
     +-	git show-index <empty.idx >actual &&
     +-	test_must_be_empty actual
     +-'
     +-
     +-test_expect_success 'pack reuse respects --incremental' '
     +-	reusable_pack --incremental >empty.pack &&
     +-	git index-pack empty.pack &&
     +-	git show-index <empty.idx >actual &&
     +-	test_must_be_empty actual
     +-'
     +-
     +-test_expect_success 'truncated bitmap fails gracefully (ewah)' '
     +-	test_config pack.writebitmaphashcache false &&
     +-	git repack -ad &&
     +-	git rev-list --use-bitmap-index --count --all >expect &&
     +-	bitmap=$(ls .git/objects/pack/*.bitmap) &&
     +-	test_when_finished "rm -f $bitmap" &&
     +-	test_copy_bytes 256 <$bitmap >$bitmap.tmp &&
     +-	mv -f $bitmap.tmp $bitmap &&
     +-	git rev-list --use-bitmap-index --count --all >actual 2>stderr &&
     +-	test_cmp expect actual &&
     +-	test_i18ngrep corrupt.ewah.bitmap stderr
     +-'
     +-
     +-test_expect_success 'truncated bitmap fails gracefully (cache)' '
     +-	git repack -ad &&
     +-	git rev-list --use-bitmap-index --count --all >expect &&
     +-	bitmap=$(ls .git/objects/pack/*.bitmap) &&
     +-	test_when_finished "rm -f $bitmap" &&
     +-	test_copy_bytes 512 <$bitmap >$bitmap.tmp &&
     +-	mv -f $bitmap.tmp $bitmap &&
     +-	git rev-list --use-bitmap-index --count --all >actual 2>stderr &&
     +-	test_cmp expect actual &&
     +-	test_i18ngrep corrupted.bitmap.index stderr
     +-'
     +-
     +-# Create a state of history with these properties:
     +-#
     +-#  - refs that allow a client to fetch some new history, while sharing some old
     +-#    history with the server; we use branches delta-reuse-old and
     +-#    delta-reuse-new here
     +-#
     +-#  - the new history contains an object that is stored on the server as a delta
     +-#    against a base that is in the old history
     +-#
     +-#  - the base object is not immediately reachable from the tip of the old
     +-#    history; finding it would involve digging down through history we know the
     +-#    other side has
     +-#
     +-# This should result in a state where fetching from old->new would not
     +-# traditionally reuse the on-disk delta (because we'd have to dig to realize
     +-# that the client has it), but we will do so if bitmaps can tell us cheaply
     +-# that the other side has it.
     +-test_expect_success 'set up thin delta-reuse parent' '
     +-	# This first commit contains the buried base object.
     +-	test-tool genrandom delta 16384 >file &&
     +-	git add file &&
     +-	git commit -m "delta base" &&
     +-	base=$(git rev-parse --verify HEAD:file) &&
     +-
     +-	# These intermediate commits bury the base back in history.
     +-	# This becomes the "old" state.
     +-	for i in 1 2 3 4 5
     +-	do
     +-		echo $i >file &&
     +-		git commit -am "intermediate $i" || return 1
     +-	done &&
     +-	git branch delta-reuse-old &&
     +-
     +-	# And now our new history has a delta against the buried base. Note
     +-	# that this must be smaller than the original file, since pack-objects
     +-	# prefers to create deltas from smaller objects to larger.
     +-	test-tool genrandom delta 16300 >file &&
     +-	git commit -am "delta result" &&
     +-	delta=$(git rev-parse --verify HEAD:file) &&
     +-	git branch delta-reuse-new &&
     +-
     +-	# Repack with bitmaps and double check that we have the expected delta
     +-	# relationship.
     +-	git repack -adb &&
     +-	have_delta $delta $base
     +-'
     +-
     +-# Now we can sanity-check the non-bitmap behavior (that the server is not able
     +-# to reuse the delta). This isn't strictly something we care about, so this
     +-# test could be scrapped in the future. But it makes sure that the next test is
     +-# actually triggering the feature we want.
     +-#
     +-# Note that our tools for working with on-the-wire "thin" packs are limited. So
     +-# we actually perform the fetch, retain the resulting pack, and inspect the
     +-# result.
     +-test_expect_success 'fetch without bitmaps ignores delta against old base' '
     +-	test_config pack.usebitmaps false &&
     +-	test_when_finished "rm -rf client.git" &&
     +-	git init --bare client.git &&
     +-	(
     +-		cd client.git &&
     +-		git config transfer.unpackLimit 1 &&
     +-		git fetch .. delta-reuse-old:delta-reuse-old &&
     +-		git fetch .. delta-reuse-new:delta-reuse-new &&
     +-		have_delta $delta $ZERO_OID
     +-	)
     +-'
     +-
     +-# And do the same for the bitmap case, where we do expect to find the delta.
     +-test_expect_success 'fetch with bitmaps can reuse old base' '
     +-	test_config pack.usebitmaps true &&
     +-	test_when_finished "rm -rf client.git" &&
     +-	git init --bare client.git &&
     +-	(
     +-		cd client.git &&
     +-		git config transfer.unpackLimit 1 &&
     +-		git fetch .. delta-reuse-old:delta-reuse-old &&
     +-		git fetch .. delta-reuse-new:delta-reuse-new &&
     +-		have_delta $delta $base
     +-	)
     +-'
     +-
     +-test_expect_success 'pack.preferBitmapTips' '
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     +-
     +-		# create enough commits that not all are receive bitmap
     +-		# coverage even if they are all at the tip of some reference.
     +-		test_commit_bulk --message="%s" 103 &&
     +-
     +-		git rev-list HEAD >commits.raw &&
     +-		sort <commits.raw >commits &&
     +-
     +-		git log --format="create refs/tags/%s %H" HEAD >refs &&
     +-		git update-ref --stdin <refs &&
     +-
     +-		git repack -adb &&
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-
     +-		# remember which commits did not receive bitmaps
     +-		comm -13 bitmaps commits >before &&
     +-		test_file_not_empty before &&
     +-
     +-		# mark the commits which did not receive bitmaps as preferred,
     +-		# and generate the bitmap again
     +-		perl -pe "s{^}{create refs/tags/include/$. }" <before |
     +-			git update-ref --stdin &&
     +-		git -c pack.preferBitmapTips=refs/tags/include repack -adb &&
     +-
     +-		# finally, check that the commit(s) without bitmap coverage
     +-		# are not the same ones as before
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-		comm -13 bitmaps commits >after &&
     +-
     +-		! test_cmp before after
     +-	)
     +-'
     +-
     +-test_expect_success 'complains about multiple pack bitmaps' '
     +-	rm -fr repo &&
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     +-
     +-		test_commit base &&
     +-
     +-		git repack -adb &&
     +-		bitmap="$(ls .git/objects/pack/pack-*.bitmap)" &&
     +-		mv "$bitmap" "$bitmap.bak" &&
     +-
     +-		test_commit other &&
     +-		git repack -ab &&
     +-
     +-		mv "$bitmap.bak" "$bitmap" &&
     +-
     +-		find .git/objects/pack -type f -name "*.pack" >packs &&
     +-		find .git/objects/pack -type f -name "*.bitmap" >bitmaps &&
     +-		test_line_count = 2 packs &&
     +-		test_line_count = 2 bitmaps &&
     +-
     +-		git rev-list --use-bitmap-index HEAD 2>err &&
     +-		grep "ignoring extra bitmap file" err
     +-	)
     ++test_expect_success 'verify writing bitmap lookup table when enabled' '
     ++	GIT_TRACE2_EVENT="$(pwd)/trace2" \
     ++		git repack -ad &&
     ++	grep "\"label\":\"writing_lookup_table\"" trace2
       '
       
     - basic_bitmap_tests
     + test_done
     +
     + ## t/t5311-pack-bitmaps-shallow.sh ##
     +@@ t/t5311-pack-bitmaps-shallow.sh: test_description='check bitmap operation with shallow repositories'
     + # the tree for A. But in a shallow one, we've grafted away
     + # A, and fetching A to B requires that the other side send
     + # us the tree for file=1.
     +-test_expect_success 'setup shallow repo' '
     +-	echo 1 >file &&
     +-	git add file &&
     +-	git commit -m orig &&
     +-	echo 2 >file &&
     +-	git commit -a -m update &&
     +-	git clone --no-local --bare --depth=1 . shallow.git &&
     +-	echo 1 >file &&
     +-	git commit -a -m repeat
     +-'
     +-
     +-test_expect_success 'turn on bitmaps in the parent' '
     +-	git repack -adb
     +-'
     +-
     +-test_expect_success 'shallow fetch from bitmapped repo' '
     +-	(cd shallow.git && git fetch)
     +-'
     ++test_shallow_bitmaps () {
     ++	writeLookupTable=false
     ++
     ++	for i in "$@"
     ++	do
     ++		case $i in
     ++		"pack.writeBitmapLookupTable") writeLookupTable=true;;
     ++		esac
     ++	done
     ++
     ++	test_expect_success 'setup shallow repo' '
     ++		rm -rf * .git &&
     ++		git init &&
     ++		git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     ++		echo 1 >file &&
     ++		git add file &&
     ++		git commit -m orig &&
     ++		echo 2 >file &&
     ++		git commit -a -m update &&
     ++		git clone --no-local --bare --depth=1 . shallow.git &&
     ++		echo 1 >file &&
     ++		git commit -a -m repeat
     ++	'
     ++
     ++	test_expect_success 'turn on bitmaps in the parent' '
     ++		git repack -adb
     ++	'
     ++
     ++	test_expect_success 'shallow fetch from bitmapped repo' '
     ++		(cd shallow.git && git fetch)
     ++	'
     ++}
     ++
     ++test_shallow_bitmaps
     ++
     + 
     + test_done
      
       ## t/t5326-multi-pack-bitmaps.sh ##
     -@@ t/t5326-multi-pack-bitmaps.sh: test_expect_success 'graceful fallback when missing reverse index' '
     - 	)
     - '
     +@@ t/t5326-multi-pack-bitmaps.sh: GIT_TEST_MULTI_PACK_INDEX_WRITE_BITMAP=0
     + sane_unset GIT_TEST_MIDX_WRITE_REV
     + sane_unset GIT_TEST_MIDX_READ_RIDX
       
     +-midx_bitmap_core
     +-
     + bitmap_reuse_tests() {
     + 	from=$1
     + 	to=$2
     ++	writeLookupTable=false
     ++
     ++	for i in $3-${$#}
     ++	do
     ++		case $i in
     ++		"pack.writeBitmapLookupTable") writeLookupTable=true;;
     ++		esac
     ++	done
     + 
     + 	test_expect_success "setup pack reuse tests ($from -> $to)" '
     + 		rm -fr repo &&
     + 		git init repo &&
     + 		(
     + 			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 			test_commit_bulk 16 &&
     + 			git tag old-tip &&
     + 
     +@@ t/t5326-multi-pack-bitmaps.sh: bitmap_reuse_tests() {
     + 	test_expect_success "build bitmap from existing ($from -> $to)" '
     + 		(
     + 			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 			test_commit_bulk --id=further 16 &&
     + 			git tag new-tip &&
     + 
     +@@ t/t5326-multi-pack-bitmaps.sh: bitmap_reuse_tests() {
     + 	test_expect_success "verify resulting bitmaps ($from -> $to)" '
     + 		(
     + 			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 			git for-each-ref &&
     + 			git rev-list --test-bitmap refs/tags/old-tip &&
     + 			git rev-list --test-bitmap refs/tags/new-tip
     +@@ t/t5326-multi-pack-bitmaps.sh: bitmap_reuse_tests() {
     + 	'
     + }
     + 
     +-bitmap_reuse_tests 'pack' 'MIDX'
     +-bitmap_reuse_tests 'MIDX' 'pack'
     +-bitmap_reuse_tests 'MIDX' 'MIDX'
     ++test_midx_bitmap_cases () {
     ++	writeLookupTable=false
     ++	writeBitmapLookupTable=
     ++
     ++	for i in "$@"
     ++	do
     ++		case $i in
     ++		"pack.writeBitmapLookupTable")
     ++			writeLookupTable=true
     ++			writeBitmapLookupTable="$i"
     ++			;;
     ++		esac
     ++	done
     ++
     ++	test_expect_success 'setup test_repository' '
     ++		rm -rf * .git &&
     ++		git init &&
     ++		git config pack.writeBitmapLookupTable '"$writeLookupTable"'
     ++	'
     + 
     +-test_expect_success 'missing object closure fails gracefully' '
     +-	rm -fr repo &&
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     ++	midx_bitmap_core
     + 
     +-		test_commit loose &&
     +-		test_commit packed &&
     ++	bitmap_reuse_tests 'pack' 'MIDX' "$writeBitmapLookupTable"
     ++	bitmap_reuse_tests 'MIDX' 'pack' "$writeBitmapLookupTable"
     ++	bitmap_reuse_tests 'MIDX' 'MIDX' "$writeBitmapLookupTable"
     + 
     +-		# Do not pass "--revs"; we want a pack without the "loose"
     +-		# commit.
     +-		git pack-objects $objdir/pack/pack <<-EOF &&
     +-		$(git rev-parse packed)
     +-		EOF
     ++	test_expect_success 'missing object closure fails gracefully' '
     ++		rm -fr repo &&
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 
     +-		test_must_fail git multi-pack-index write --bitmap 2>err &&
     +-		grep "doesn.t have full closure" err &&
     +-		test_path_is_missing $midx
     +-	)
     +-'
     ++			test_commit loose &&
     ++			test_commit packed &&
     + 
     +-midx_bitmap_partial_tests
     ++			# Do not pass "--revs"; we want a pack without the "loose"
     ++			# commit.
     ++			git pack-objects $objdir/pack/pack <<-EOF &&
     ++			$(git rev-parse packed)
     ++			EOF
     + 
     +-test_expect_success 'removing a MIDX clears stale bitmaps' '
     +-	rm -fr repo &&
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     +-		test_commit base &&
     +-		git repack &&
     +-		git multi-pack-index write --bitmap &&
     ++			test_must_fail git multi-pack-index write --bitmap 2>err &&
     ++			grep "doesn.t have full closure" err &&
     ++			test_path_is_missing $midx
     ++		)
     ++	'
     + 
     +-		# Write a MIDX and bitmap; remove the MIDX but leave the bitmap.
     +-		stale_bitmap=$midx-$(midx_checksum $objdir).bitmap &&
     +-		rm $midx &&
     ++	midx_bitmap_partial_tests
     + 
     +-		# Then write a new MIDX.
     +-		test_commit new &&
     +-		git repack &&
     +-		git multi-pack-index write --bitmap &&
     ++	test_expect_success 'removing a MIDX clears stale bitmaps' '
     ++		rm -fr repo &&
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     ++			test_commit base &&
     ++			git repack &&
     ++			git multi-pack-index write --bitmap &&
     ++
     ++			# Write a MIDX and bitmap; remove the MIDX but leave the bitmap.
     ++			stale_bitmap=$midx-$(midx_checksum $objdir).bitmap &&
     ++			rm $midx &&
     ++
     ++			# Then write a new MIDX.
     ++			test_commit new &&
     ++			git repack &&
     ++			git multi-pack-index write --bitmap &&
     ++
     ++			test_path_is_file $midx &&
     ++			test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     ++			test_path_is_missing $stale_bitmap
     ++		)
     ++	'
     + 
     +-		test_path_is_file $midx &&
     +-		test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     +-		test_path_is_missing $stale_bitmap
     +-	)
     +-'
     ++	test_expect_success 'pack.preferBitmapTips' '
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 
     +-test_expect_success 'pack.preferBitmapTips' '
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     ++			test_commit_bulk --message="%s" 103 &&
     + 
     +-		test_commit_bulk --message="%s" 103 &&
     ++			git log --format="%H" >commits.raw &&
     ++			sort <commits.raw >commits &&
     + 
     +-		git log --format="%H" >commits.raw &&
     +-		sort <commits.raw >commits &&
     ++			git log --format="create refs/tags/%s %H" HEAD >refs &&
     ++			git update-ref --stdin <refs &&
     + 
     +-		git log --format="create refs/tags/%s %H" HEAD >refs &&
     +-		git update-ref --stdin <refs &&
     ++			git multi-pack-index write --bitmap &&
     ++			test_path_is_file $midx &&
     ++			test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     + 
     +-		git multi-pack-index write --bitmap &&
     +-		test_path_is_file $midx &&
     +-		test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++			comm -13 bitmaps commits >before &&
     ++			test_line_count = 1 before &&
     + 
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-		comm -13 bitmaps commits >before &&
     +-		test_line_count = 1 before &&
     ++			perl -ne "printf(\"create refs/tags/include/%d \", $.); print" \
     ++				<before | git update-ref --stdin &&
     + 
     +-		perl -ne "printf(\"create refs/tags/include/%d \", $.); print" \
     +-			<before | git update-ref --stdin &&
     ++			rm -fr $midx-$(midx_checksum $objdir).bitmap &&
     ++			rm -fr $midx &&
     + 
     +-		rm -fr $midx-$(midx_checksum $objdir).bitmap &&
     +-		rm -fr $midx &&
     ++			git -c pack.preferBitmapTips=refs/tags/include \
     ++				multi-pack-index write --bitmap &&
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++			comm -13 bitmaps commits >after &&
     + 
     +-		git -c pack.preferBitmapTips=refs/tags/include \
     +-			multi-pack-index write --bitmap &&
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-		comm -13 bitmaps commits >after &&
     ++			! test_cmp before after
     ++		)
     ++	'
     + 
     +-		! test_cmp before after
     +-	)
     +-'
     ++	test_expect_success 'writing a bitmap with --refs-snapshot' '
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 
     +-test_expect_success 'writing a bitmap with --refs-snapshot' '
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     ++			test_commit one &&
     ++			test_commit two &&
     + 
     +-		test_commit one &&
     +-		test_commit two &&
     ++			git rev-parse one >snapshot &&
     + 
     +-		git rev-parse one >snapshot &&
     ++			git repack -ad &&
     + 
     +-		git repack -ad &&
     ++			# First, write a MIDX which see both refs/tags/one and
     ++			# refs/tags/two (causing both of those commits to receive
     ++			# bitmaps).
     ++			git multi-pack-index write --bitmap &&
     + 
     +-		# First, write a MIDX which see both refs/tags/one and
     +-		# refs/tags/two (causing both of those commits to receive
     +-		# bitmaps).
     +-		git multi-pack-index write --bitmap &&
     ++			test_path_is_file $midx &&
     ++			test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     + 
     +-		test_path_is_file $midx &&
     +-		test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++			grep "$(git rev-parse one)" bitmaps &&
     ++			grep "$(git rev-parse two)" bitmaps &&
     + 
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-		grep "$(git rev-parse one)" bitmaps &&
     +-		grep "$(git rev-parse two)" bitmaps &&
     ++			rm -fr $midx-$(midx_checksum $objdir).bitmap &&
     ++			rm -fr $midx &&
     + 
     +-		rm -fr $midx-$(midx_checksum $objdir).bitmap &&
     +-		rm -fr $midx &&
     ++			# Then again, but with a refs snapshot which only sees
     ++			# refs/tags/one.
     ++			git multi-pack-index write --bitmap --refs-snapshot=snapshot &&
     + 
     +-		# Then again, but with a refs snapshot which only sees
     +-		# refs/tags/one.
     +-		git multi-pack-index write --bitmap --refs-snapshot=snapshot &&
     ++			test_path_is_file $midx &&
     ++			test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     + 
     +-		test_path_is_file $midx &&
     +-		test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++			grep "$(git rev-parse one)" bitmaps &&
     ++			! grep "$(git rev-parse two)" bitmaps
     ++		)
     ++	'
     + 
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-		grep "$(git rev-parse one)" bitmaps &&
     +-		! grep "$(git rev-parse two)" bitmaps
     +-	)
     +-'
     ++	test_expect_success 'write a bitmap with --refs-snapshot (preferred tips)' '
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 
     +-test_expect_success 'write a bitmap with --refs-snapshot (preferred tips)' '
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     ++			test_commit_bulk --message="%s" 103 &&
     + 
     +-		test_commit_bulk --message="%s" 103 &&
     ++			git log --format="%H" >commits.raw &&
     ++			sort <commits.raw >commits &&
     + 
     +-		git log --format="%H" >commits.raw &&
     +-		sort <commits.raw >commits &&
     ++			git log --format="create refs/tags/%s %H" HEAD >refs &&
     ++			git update-ref --stdin <refs &&
     + 
     +-		git log --format="create refs/tags/%s %H" HEAD >refs &&
     +-		git update-ref --stdin <refs &&
     ++			git multi-pack-index write --bitmap &&
     ++			test_path_is_file $midx &&
     ++			test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     + 
     +-		git multi-pack-index write --bitmap &&
     +-		test_path_is_file $midx &&
     +-		test_path_is_file $midx-$(midx_checksum $objdir).bitmap &&
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++			comm -13 bitmaps commits >before &&
     ++			test_line_count = 1 before &&
     + 
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-		comm -13 bitmaps commits >before &&
     +-		test_line_count = 1 before &&
     ++			(
     ++				grep -vf before commits.raw &&
     ++				# mark missing commits as preferred
     ++				sed "s/^/+/" before
     ++			) >snapshot &&
     + 
     ++			rm -fr $midx-$(midx_checksum $objdir).bitmap &&
     ++			rm -fr $midx &&
     ++
     ++			git multi-pack-index write --bitmap --refs-snapshot=snapshot &&
     ++			test-tool bitmap list-commits | sort >bitmaps &&
     ++			comm -13 bitmaps commits >after &&
     ++
     ++			! test_cmp before after
     ++		)
     ++	'
     ++
     ++	test_expect_success 'hash-cache values are propagated from pack bitmaps' '
     ++		rm -fr repo &&
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     + 		(
     +-			grep -vf before commits.raw &&
     +-			# mark missing commits as preferred
     +-			sed "s/^/+/" before
     +-		) >snapshot &&
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 
     +-		rm -fr $midx-$(midx_checksum $objdir).bitmap &&
     +-		rm -fr $midx &&
     ++			test_commit base &&
     ++			test_commit base2 &&
     ++			git repack -adb &&
     + 
     +-		git multi-pack-index write --bitmap --refs-snapshot=snapshot &&
     +-		test-tool bitmap list-commits | sort >bitmaps &&
     +-		comm -13 bitmaps commits >after &&
     ++			test-tool bitmap dump-hashes >pack.raw &&
     ++			test_file_not_empty pack.raw &&
     ++			sort pack.raw >pack.hashes &&
     + 
     +-		! test_cmp before after
     +-	)
     +-'
     ++			test_commit new &&
     ++			git repack &&
     ++			git multi-pack-index write --bitmap &&
     + 
     +-test_expect_success 'hash-cache values are propagated from pack bitmaps' '
     +-	rm -fr repo &&
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     ++			test-tool bitmap dump-hashes >midx.raw &&
     ++			sort midx.raw >midx.hashes &&
     + 
     +-		test_commit base &&
     +-		test_commit base2 &&
     +-		git repack -adb &&
     ++			# ensure that every namehash in the pack bitmap can be found in
     ++			# the midx bitmap (i.e., that there are no oid-namehash pairs
     ++			# unique to the pack bitmap).
     ++			comm -23 pack.hashes midx.hashes >dropped.hashes &&
     ++			test_must_be_empty dropped.hashes
     ++		)
     ++	'
     + 
     +-		test-tool bitmap dump-hashes >pack.raw &&
     +-		test_file_not_empty pack.raw &&
     +-		sort pack.raw >pack.hashes &&
     ++	test_expect_success 'no .bitmap is written without any objects' '
     ++		rm -fr repo &&
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 
     +-		test_commit new &&
     +-		git repack &&
     +-		git multi-pack-index write --bitmap &&
     ++			empty="$(git pack-objects $objdir/pack/pack </dev/null)" &&
     ++			cat >packs <<-EOF &&
     ++			pack-$empty.idx
     ++			EOF
     + 
     +-		test-tool bitmap dump-hashes >midx.raw &&
     +-		sort midx.raw >midx.hashes &&
     ++			git multi-pack-index write --bitmap --stdin-packs \
     ++				<packs 2>err &&
     + 
     +-		# ensure that every namehash in the pack bitmap can be found in
     +-		# the midx bitmap (i.e., that there are no oid-namehash pairs
     +-		# unique to the pack bitmap).
     +-		comm -23 pack.hashes midx.hashes >dropped.hashes &&
     +-		test_must_be_empty dropped.hashes
     +-	)
     +-'
     ++			grep "bitmap without any objects" err &&
     + 
     +-test_expect_success 'no .bitmap is written without any objects' '
     +-	rm -fr repo &&
     +-	git init repo &&
     +-	test_when_finished "rm -fr repo" &&
     +-	(
     +-		cd repo &&
     ++			test_path_is_file $midx &&
     ++			test_path_is_missing $midx-$(midx_checksum $objdir).bitmap
     ++		)
     ++	'
     ++
     ++	test_expect_success 'graceful fallback when missing reverse index' '
     ++		rm -fr repo &&
     ++		git init repo &&
     ++		test_when_finished "rm -fr repo" &&
     ++		(
     ++			cd repo &&
     ++			git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 
     +-		empty="$(git pack-objects $objdir/pack/pack </dev/null)" &&
     +-		cat >packs <<-EOF &&
     +-		pack-$empty.idx
     +-		EOF
     ++			test_commit base &&
     + 
     +-		git multi-pack-index write --bitmap --stdin-packs \
     +-			<packs 2>err &&
     ++			# write a pack and MIDX bitmap containing base
     ++			git repack -adb &&
     ++			git multi-pack-index write --bitmap &&
     + 
     +-		grep "bitmap without any objects" err &&
     ++			GIT_TEST_MIDX_READ_RIDX=0 \
     ++				git rev-list --use-bitmap-index HEAD 2>err &&
     ++			! grep "ignoring extra bitmap file" err
     ++		)
     ++	'
     ++}
     + 
     +-		test_path_is_file $midx &&
     +-		test_path_is_missing $midx-$(midx_checksum $objdir).bitmap
     +-	)
     +-'
     ++test_midx_bitmap_cases
     ++
     ++test_midx_bitmap_cases "pack.writeBitmapLookupTable"
     + 
     +-test_expect_success 'graceful fallback when missing reverse index' '
      +test_expect_success 'multi-pack-index write writes lookup table if enabled' '
     -+	rm -fr repo &&
     -+	git init repo &&
     -+	test_when_finished "rm -fr repo" &&
     -+	(
     -+		cd repo &&
     -+		test_commit base &&
     + 	rm -fr repo &&
     + 	git init repo &&
     + 	test_when_finished "rm -fr repo" &&
     + 	(
     + 		cd repo &&
     +-
     + 		test_commit base &&
     +-
     +-		# write a pack and MIDX bitmap containing base
     +-		git repack -adb &&
     +-		git multi-pack-index write --bitmap &&
     +-
     +-		GIT_TEST_MIDX_READ_RIDX=0 \
     +-			git rev-list --use-bitmap-index HEAD 2>err &&
     +-		! grep "ignoring extra bitmap file" err
     ++		git config pack.writeBitmapLookupTable true &&
      +		git repack -ad &&
      +		GIT_TRACE2_EVENT="$(pwd)/trace" \
      +			git multi-pack-index write --bitmap &&
      +		grep "\"label\":\"writing_lookup_table\"" trace
     -+	)
     + 	)
     + '
     + 
     +
     + ## t/t5327-multi-pack-bitmaps-rev.sh ##
     +@@ t/t5327-multi-pack-bitmaps-rev.sh: export GIT_TEST_MIDX_READ_RIDX
     + midx_bitmap_core rev
     + midx_bitmap_partial_tests rev
     + 
     ++test_expect_success 'reinitialize the repository with lookup table enabled' '
     ++    rm -fr * .git &&
     ++    git init &&
     ++    git config pack.writeBitmapLookupTable true
      +'
     ++
     ++midx_bitmap_core rev
     ++midx_bitmap_partial_tests rev
     ++
       test_done
 4:  4fbfcff8a20 ! 4:  e64362621d2 pack-bitmap: prepare to read lookup table extension
     @@ Commit message
          does not know how to parse them.
      
          Teach Git to parse the existing bitmap lookup table. The older
     -    versions of git are not affected by it. Those versions ignore the
     +    versions of Git are not affected by it. Those versions ignore the
          lookup table.
      
     -    Signed-off-by: Abhradeep Chakraborty <[email protected]>
          Mentored-by: Taylor Blau <[email protected]>
          Co-Mentored-by: Kaartic Sivaraam <[email protected]>
     +    Signed-off-by: Abhradeep Chakraborty <[email protected]>
      
       ## pack-bitmap.c ##
      @@ pack-bitmap.c: struct bitmap_index {
     @@ pack-bitmap.c: struct bitmap_index {
       
      +	/*
      +	 * If not NULL, this point into the commit table extension
     -+	 * (within map).
     ++	 * (within the memory mapped region `map`).
      +	 */
      +	unsigned char *table_lookup;
      +
     @@ pack-bitmap.c: static int load_bitmap_header(struct bitmap_index *index)
       			index_end -= cache_size;
       		}
      +
     -+		if (flags & BITMAP_OPT_LOOKUP_TABLE &&
     -+			git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1)) {
     -+			size_t table_size = 0;
     -+			size_t triplet_sz = st_add3(sizeof(uint32_t),    /* commit position */
     -+							sizeof(uint64_t),    /* offset */
     -+							sizeof(uint32_t));    /* xor offset */
     -+
     -+			table_size = st_add(table_size,
     -+					st_mult(ntohl(header->entry_count),
     -+						triplet_sz));
     ++		if (flags & BITMAP_OPT_LOOKUP_TABLE) {
     ++			size_t table_size = st_mult(ntohl(header->entry_count),
     ++						    BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH);
      +			if (table_size > index_end - index->map - header_size)
     -+				return error("corrupted bitmap index file (too short to fit lookup table)");
     -+			index->table_lookup = (void *)(index_end - table_size);
     ++				return error(_("corrupted bitmap index file (too short to fit lookup table)"));
     ++			if (git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1))
     ++				index->table_lookup = (void *)(index_end - table_size);
      +			index_end -= table_size;
      +		}
       	}
     @@ pack-bitmap.c: static struct stored_bitmap *store_bitmap(struct bitmap_index *in
      -	/* a 0 return code means the insertion succeeded with no changes,
      -	 * because the SHA1 already existed on the map. this is bad, there
      -	 * shouldn't be duplicated commits in the index */
     -+	/* A 0 return code means the insertion succeeded with no changes,
     -+	 * because the SHA1 already existed on the map. If lookup table
     -+	 * is NULL, this is bad, there shouldn't be duplicated commits
     -+	 * in the index.
     -+	 *
     -+	 * If table_lookup exists, that means the desired bitmap is already
     -+	 * loaded. Either this bitmap has been stored directly or another
     -+	 * bitmap has a direct or indirect xor relation with it. */
     ++	/*
     ++	 * A 0 return code means the insertion succeeded with no changes,
     ++	 * because the SHA1 already existed on the map. This is bad, there
     ++	 * shouldn't be duplicated commits in the index.
     ++	 */
       	if (ret == 0) {
      -		error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
     --		return NULL;
     -+		if (!index->table_lookup) {
     -+			error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
     -+			return NULL;
     -+		}
     -+		return kh_value(index->bitmaps, hash_pos);
     ++		error(_("duplicate entry in bitmap index: %s"), oid_to_hex(oid));
     + 		return NULL;
       	}
       
     - 	kh_value(index->bitmaps, hash_pos) = stored;
      @@ pack-bitmap.c: static int load_bitmap(struct bitmap_index *bitmap_git)
       		!(bitmap_git->tags = read_bitmap_1(bitmap_git)))
       		goto failed;
     @@ pack-bitmap.c: struct include_data {
       	struct bitmap *seen;
       };
       
     -+static inline const void *bitmap_get_triplet(struct bitmap_index *bitmap_git, uint32_t xor_pos)
     -+{
     -+	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));
     -+	const void *p = bitmap_git->table_lookup + st_mult(xor_pos, triplet_sz);
     -+	return p;
     -+}
     -+
     -+static uint64_t triplet_get_offset(const void *triplet)
     -+{
     -+	const void *p = (unsigned char*) triplet + sizeof(uint32_t);
     -+	return get_be64(p);
     -+}
     ++struct bitmap_lookup_table_triplet {
     ++	uint32_t commit_pos;
     ++	uint64_t offset;
     ++	uint32_t xor_row;
     ++};
      +
     -+static uint32_t triplet_get_xor_pos(const void *triplet)
     ++struct bitmap_lookup_table_xor_item {
     ++	struct object_id oid;
     ++	uint64_t offset;
     ++};
     ++
     ++/*
     ++ * This function gets the raw triplet from `row`'th row in the
     ++ * lookup table and fills that data to the `triplet`.
     ++ */
     ++static int lookup_table_get_triplet(struct bitmap_index *bitmap_git,
     ++				    uint32_t pos,
     ++				    struct bitmap_lookup_table_triplet *triplet)
      +{
     -+	const void *p = (unsigned char*) triplet + st_add(sizeof(uint32_t), sizeof(uint64_t));
     -+	return get_be32(p);
     ++	unsigned char *p = NULL;
     ++	if (pos >= bitmap_git->entry_count)
     ++		return error(_("corrupt bitmap lookup table: triplet position out of index"));
     ++
     ++	p = bitmap_git->table_lookup + st_mult(pos, BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH);
     ++
     ++	triplet->commit_pos = get_be32(p);
     ++	p += sizeof(uint32_t);
     ++	triplet->offset = get_be64(p);
     ++	p += sizeof(uint64_t);
     ++	triplet->xor_row = get_be32(p);
     ++	return 0;
      +}
      +
     ++/*
     ++ * Searches for a matching triplet. `va` is a pointer
     ++ * to the wanted commit position value. `vb` points to
     ++ * a triplet in lookup table. The first 4 bytes of each
     ++ * triplet (pointed by `vb`) are compared with `*va`.
     ++ */
      +static int triplet_cmp(const void *va, const void *vb)
      +{
     -+	int result = 0;
     -+	uint32_t *a = (uint32_t *) va;
     ++
     ++	uint32_t a = *(uint32_t *)va;
      +	uint32_t b = get_be32(vb);
     -+	if (*a > b)
     -+		result = 1;
     -+	else if (*a < b)
     -+		result = -1;
     -+	else
     -+		result = 0;
     ++	if (a > b)
     ++		return 1;
     ++	else if (a < b)
     ++		return -1;
      +
     -+	return result;
     ++	return 0;
      +}
      +
     -+static uint32_t bsearch_pos(struct bitmap_index *bitmap_git, struct object_id *oid,
     -+						uint32_t *result)
     ++static uint32_t bsearch_pos(struct bitmap_index *bitmap_git,
     ++			    struct object_id *oid,
     ++			    uint32_t *result)
      +{
      +	int found;
      +
     -+	if (bitmap_git->midx)
     ++	if (bitmap_is_midx(bitmap_git))
      +		found = bsearch_midx(oid, bitmap_git->midx, result);
      +	else
      +		found = bsearch_pack(oid, bitmap_git->pack, result);
     @@ pack-bitmap.c: struct include_data {
      +	return found;
      +}
      +
     ++/*
     ++ * `bsearch_triplet` function searches for the raw triplet having
     ++ * commit position same as `commit_pos` and fills `triplet`
     ++ * object from the raw triplet. Returns 1 on success and 0
     ++ * on failure.
     ++ */
     ++static int bsearch_triplet(uint32_t *commit_pos,
     ++			   struct bitmap_index *bitmap_git,
     ++			   struct bitmap_lookup_table_triplet *triplet)
     ++{
     ++	unsigned char *p = bsearch(commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
     ++				   BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH, triplet_cmp);
     ++
     ++	if (!p)
     ++		return 0;
     ++	triplet->commit_pos = get_be32(p);
     ++	p += sizeof(uint32_t);
     ++	triplet->offset = get_be64(p);
     ++	p += sizeof(uint64_t);
     ++	triplet->xor_row = get_be32(p);
     ++	return 1;
     ++}
     ++
      +static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
      +					  struct commit *commit)
      +{
     -+	uint32_t commit_pos, xor_pos;
     ++	uint32_t commit_pos, xor_row;
      +	uint64_t offset;
      +	int flags;
     -+	const void *triplet = NULL;
     ++	struct bitmap_lookup_table_triplet triplet;
      +	struct object_id *oid = &commit->object.oid;
      +	struct ewah_bitmap *bitmap;
      +	struct stored_bitmap *xor_bitmap = NULL;
     -+	size_t triplet_sz = st_add3(sizeof(uint32_t), sizeof(uint64_t), sizeof(uint32_t));
      +
      +	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
      +
      +	if (!found)
      +		return NULL;
      +
     -+	triplet = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
     -+						triplet_sz, triplet_cmp);
     -+	if (!triplet)
     ++	if (!bsearch_triplet(&commit_pos, bitmap_git, &triplet))
      +		return NULL;
      +
     -+	offset = triplet_get_offset(triplet);
     -+	xor_pos = triplet_get_xor_pos(triplet);
     ++	offset = triplet.offset;
     ++	xor_row = triplet.xor_row;
      +
     -+	if (xor_pos != 0xffffffff) {
     ++	if (xor_row != 0xffffffff) {
      +		int xor_flags;
     ++		khiter_t hash_pos;
      +		uint64_t offset_xor;
     -+		uint32_t *xor_positions;
     -+		struct object_id xor_oid;
     -+		size_t size = 0;
     -+
     -+		ALLOC_ARRAY(xor_positions, bitmap_git->entry_count);
     -+		while (xor_pos != 0xffffffff) {
     -+			xor_positions[size++] = xor_pos;
     -+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
     -+			xor_pos = triplet_get_xor_pos(triplet);
     ++		struct bitmap_lookup_table_xor_item *xor_items;
     ++		struct bitmap_lookup_table_xor_item xor_item;
     ++		size_t xor_items_nr = 0, xor_items_alloc = 64;
     ++
     ++		ALLOC_ARRAY(xor_items, xor_items_alloc);
     ++		while (xor_row != 0xffffffff) {
     ++			struct object_id xor_oid;
     ++
     ++			if (xor_items_nr + 1 >= bitmap_git->entry_count) {
     ++				free(xor_items);
     ++				error(_("corrupt bitmap lookup table: xor chain exceed entry count"));
     ++				return NULL;
     ++			}
     ++
     ++			if (lookup_table_get_triplet(bitmap_git, xor_row, &triplet) < 0)
     ++				return NULL;
     ++
     ++			offset_xor = triplet.offset;
     ++
     ++			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, triplet.commit_pos) < 0) {
     ++				free(xor_items);
     ++				error(_("corrupt bitmap lookup table: commit index %u out of range"),
     ++					triplet.commit_pos);
     ++				return NULL;
     ++			}
     ++
     ++			hash_pos = kh_get_oid_map(bitmap_git->bitmaps, xor_oid);
     ++
     ++			/*
     ++			 * If desired bitmap is already stored, we don't need
     ++			 * to iterate further. Because we know that bitmaps
     ++			 * that are needed to be parsed to parse this bitmap
     ++			 * has already been stored. So, assign this stored bitmap
     ++			 * to the xor_bitmap.
     ++			 */
     ++			if (hash_pos < kh_end(bitmap_git->bitmaps) &&
     ++			    (xor_bitmap = kh_value(bitmap_git->bitmaps, hash_pos)))
     ++				break;
     ++
     ++			ALLOC_GROW(xor_items, xor_items_nr + 1, xor_items_alloc);
     ++			xor_items[xor_items_nr++] = (struct bitmap_lookup_table_xor_item) {.oid = xor_oid,
     ++											   .offset = offset_xor};
     ++			xor_row = triplet.xor_row;
      +		}
      +
     -+		while (size){
     -+			xor_pos = xor_positions[size - 1];
     -+			triplet = bitmap_get_triplet(bitmap_git, xor_pos);
     -+			commit_pos = get_be32(triplet);
     -+			offset_xor = triplet_get_offset(triplet);
     ++		while (xor_items_nr) {
     ++			xor_item = xor_items[xor_items_nr - 1];
     ++			offset_xor = xor_item.offset;
      +
     -+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, commit_pos) < 0) {
     -+				free(xor_positions);
     ++			bitmap_git->map_pos = offset_xor;
     ++			if (bitmap_git->map_size - bitmap_git->map_pos < 6) {
     ++				error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
     ++					oid_to_hex(&xor_item.oid));
     ++				free(xor_items);
      +				return NULL;
      +			}
      +
     -+			bitmap_git->map_pos = offset_xor + sizeof(uint32_t) + sizeof(uint8_t);
     ++			bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
      +			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
      +			bitmap = read_bitmap_1(bitmap_git);
      +
     -+			if (!bitmap){
     -+				free(xor_positions);
     ++			if (!bitmap) {
     ++				free(xor_items);
      +				return NULL;
      +			}
      +
     -+			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_oid, xor_bitmap, xor_flags);
     -+			size--;
     ++			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_item.oid, xor_bitmap, xor_flags);
     ++			xor_items_nr--;
      +		}
      +
     -+		free(xor_positions);
     ++		free(xor_items);
      +	}
      +
     -+	bitmap_git->map_pos = offset + sizeof(uint32_t) + sizeof(uint8_t);
     ++	bitmap_git->map_pos = offset;
     ++	if (bitmap_git->map_size - bitmap_git->map_pos < 6) {
     ++		error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
     ++			oid_to_hex(oid));
     ++		return NULL;
     ++	}
     ++
     ++	bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
      +	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
      +	bitmap = read_bitmap_1(bitmap_git);
      +
     @@ pack-bitmap.c: struct include_data {
      +		if (!bitmap_git->table_lookup)
      +			return NULL;
      +
     ++		trace2_region_enter("pack-bitmap", "reading_lookup_table", the_repository);
      +		/* NEEDSWORK: cache misses aren't recorded */
      +		bitmap = lazy_bitmap_for_commit(bitmap_git, commit);
     -+		if(!bitmap)
     ++		trace2_region_leave("pack-bitmap", "reading_lookup_table", the_repository);
     ++		if (!bitmap)
      +			return NULL;
      +		return lookup_stored_bitmap(bitmap);
      +	}
     @@ pack-bitmap.c: void test_bitmap_walk(struct rev_info *revs)
       		die("you must specify exactly one commit to test");
       
      -	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
     -+	fprintf(stderr, "Bitmap v%d test (%d entries)\n",
     - 		bitmap_git->version, bitmap_git->entry_count);
     +-		bitmap_git->version, bitmap_git->entry_count);
     ++	fprintf(stderr, "Bitmap v%d test (%d entries%s)",
     ++		bitmap_git->version,
     ++		bitmap_git->entry_count,
     ++		bitmap_git->table_lookup ? "" : " loaded");
       
     -+	if (!bitmap_git->table_lookup)
     -+		fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
     -+			bitmap_git->version, bitmap_git->entry_count);
     -+
       	root = revs->pending.objects[0].item;
       	bm = bitmap_for_commit(bitmap_git, (struct commit *)root);
     - 
      @@ pack-bitmap.c: void test_bitmap_walk(struct rev_info *revs)
       
       int test_bitmap_commits(struct repository *r)
       {
      -	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
     -+	struct bitmap_index *bitmap_git = NULL;
       	struct object_id oid;
       	MAYBE_UNUSED void *value;
     - 
     -+	/* As this function is only used to print bitmap selected
     ++	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
     ++
     ++	/*
     ++	 * As this function is only used to print bitmap selected
      +	 * commits, we don't have to read the commit table.
      +	 */
     -+	setenv("GIT_TEST_READ_COMMIT_TABLE", "0", 1);
     -+
     -+	bitmap_git = prepare_bitmap_git(r);
     + 
       	if (!bitmap_git)
       		die("failed to load bitmap indexes");
       
     -@@ pack-bitmap.c: int test_bitmap_commits(struct repository *r)
     ++	if (bitmap_git->table_lookup) {
     ++		if (load_bitmap_entries_v1(bitmap_git) < 0)
     ++			die(_("failed to load bitmap indexes"));
     ++	}
     ++
     + 	kh_foreach(bitmap_git->bitmaps, oid, value, {
       		printf("%s\n", oid_to_hex(&oid));
       	});
     +
     + ## pack-bitmap.h ##
     +@@ pack-bitmap.h: struct bitmap_disk_header {
       
     -+	setenv("GIT_TEST_READ_COMMIT_TABLE", "1", 1);
     - 	free_bitmap_index(bitmap_git);
     + #define NEEDS_BITMAP (1u<<22)
       
     - 	return 0;
     ++/*
     ++ * The width in bytes of a single triplet in the lookup table
     ++ * extension:
     ++ *     (commit_pos, offset, xor_row)
     ++ *
     ++ * whose fields ar 32-, 64-, 32- bits wide, respectively.
     ++ */
     ++#define BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH (16)
     ++
     + enum pack_bitmap_opts {
     + 	BITMAP_OPT_FULL_DAG = 0x1,
     + 	BITMAP_OPT_HASH_CACHE = 0x4,
      
       ## t/t5310-pack-bitmaps.sh ##
     -@@ t/t5310-pack-bitmaps.sh: test_expect_success 'full repack creates bitmaps' '
     - 	grep "\"label\":\"writing_lookup_table\"" trace
     +@@ t/t5310-pack-bitmaps.sh: test_bitmap_cases () {
     + 
     + 	test_expect_success 'truncated bitmap fails gracefully (ewah)' '
     + 		test_config pack.writebitmaphashcache false &&
     ++		test_config pack.writebitmaplookuptable false &&
     + 		git repack -ad &&
     + 		git rev-list --use-bitmap-index --count --all >expect &&
     + 		bitmap=$(ls .git/objects/pack/*.bitmap) &&
     +@@ t/t5310-pack-bitmaps.sh: test_bitmap_cases () {
     + 	'
     + 
     + 	test_expect_success 'truncated bitmap fails gracefully (cache)' '
     ++		git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
     + 		git repack -ad &&
     + 		git rev-list --use-bitmap-index --count --all >expect &&
     + 		bitmap=$(ls .git/objects/pack/*.bitmap) &&
     +@@ t/t5310-pack-bitmaps.sh: test_expect_success 'verify writing bitmap lookup table when enabled' '
     + 	grep "\"label\":\"writing_lookup_table\"" trace2
       '
       
     -+test_expect_success 'using lookup table loads only necessary bitmaps' '
     -+	git rev-list --test-bitmap HEAD 2>out &&
     -+	! grep "Bitmap v1 test (106 entries loaded)" out &&
     -+	grep "Found bitmap for" out
     ++test_expect_success 'lookup table is actually used to traverse objects' '
     ++	git repack -adb &&
     ++	GIT_TRACE2_EVENT="$(pwd)/trace3" \
     ++		git rev-list --use-bitmap-index --count --all &&
     ++	grep "\"label\":\"reading_lookup_table\"" trace3
      +'
      +
     - basic_bitmap_tests
     - 
     - test_expect_success 'incremental repack fails when bitmaps are requested' '
     -@@ t/t5310-pack-bitmaps.sh: test_expect_success 'pack reuse respects --incremental' '
     - 
     - test_expect_success 'truncated bitmap fails gracefully (ewah)' '
     - 	test_config pack.writebitmaphashcache false &&
     -+	test_config pack.writebitmaplookuptable false &&
     - 	git repack -ad &&
     - 	git rev-list --use-bitmap-index --count --all >expect &&
     - 	bitmap=$(ls .git/objects/pack/*.bitmap) &&
     -
     - ## t/t5326-multi-pack-bitmaps.sh ##
     -@@ t/t5326-multi-pack-bitmaps.sh: test_expect_success 'multi-pack-index write writes lookup table if enabled' '
     - 		grep "\"label\":\"writing_lookup_table\"" trace
     - 	)
     - '
     ++test_expect_success 'truncated bitmap fails gracefully (lookup table)' '
     ++	test_config pack.writebitmaphashcache false &&
     ++	git repack -adb &&
     ++	git rev-list --use-bitmap-index --count --all >expect &&
     ++	bitmap=$(ls .git/objects/pack/*.bitmap) &&
     ++	test_when_finished "rm -f $bitmap" &&
     ++	test_copy_bytes 512 <$bitmap >$bitmap.tmp &&
     ++	mv -f $bitmap.tmp $bitmap &&
     ++	git rev-list --use-bitmap-index --count --all >actual 2>stderr &&
     ++	test_cmp expect actual &&
     ++	test_i18ngrep corrupted.bitmap.index stderr
     ++'
      +
       test_done

5: 96c0041688f ! 5: a155c1e2eba bitmap-lookup-table: add performance tests for lookup table
@@ Metadata
## Commit message ##
bitmap-lookup-table: add performance tests for lookup table

- Add performance tests to verify the performance of lookup table.
+ Add performance tests to verify the performance of lookup table with
+ pack.writeReverseIndex enabled. This is to check the performance
+ when the above configuration is set.

Lookup table makes Git run faster in most of the cases. Below is the result of t/perf/p5310-pack-bitmaps.sh.perf/p5326-multi-pack-bitmaps.sh gives similar result. The repository used in the test is linux kernel.

Test this tree
- --------------------------------------------------------------------------
- 5310.4: repack to disk (lookup=false) 295.94(250.45+15.24)
- 5310.5: simulated clone 12.52(5.07+1.40)
- 5310.6: simulated fetch 1.89(2.94+0.24)
- 5310.7: pack to file (bitmap) 41.39(20.33+7.20)
- 5310.8: rev-list (commits) 0.98(0.59+0.12)
- 5310.9: rev-list (objects) 3.40(3.27+0.10)
+ ---------------------------------------------------------------------------
+ 5310.4: repack to disk (lookup=false) 296.55(256.53+14.52)
+ 5310.5: simulated clone 15.64(8.88+1.39)
+ 5310.6: simulated fetch 1.65(2.75+0.20)
+ 5310.7: pack to file (bitmap) 48.71(30.20+7.58)
+ 5310.8: rev-list (commits) 0.61(0.41+0.08)
+ 5310.9: rev-list (objects) 4.38(4.26+0.09)
5310.10: rev-list with tag negated via --not 0.07(0.02+0.04)
--all (objects)
- 5310.11: rev-list with negative tag (objects) 0.23(0.16+0.06)
- 5310.12: rev-list count with blob:none 0.26(0.18+0.07)
- 5310.13: rev-list count with blob:limit=1k 6.45(5.94+0.37)
- 5310.14: rev-list count with tree:0 0.26(0.18+0.07)
- 5310.15: simulated partial clone 4.99(3.19+0.45)
- 5310.19: repack to disk (lookup=true) 269.67(174.70+21.33)
- 5310.20: simulated clone 11.03(5.07+1.11)
- 5310.21: simulated fetch 0.79(0.79+0.17)
- 5310.22: pack to file (bitmap) 43.03(20.28+7.43)
- 5310.23: rev-list (commits) 0.86(0.54+0.09)
- 5310.24: rev-list (objects) 3.35(3.26+0.07)
- 5310.25: rev-list with tag negated via --not 0.05(0.00+0.03)
+ 5310.11: rev-list with negative tag (objects) 0.05(0.01+0.03)
+ 5310.12: rev-list count with blob:none 0.08(0.03+0.04)
+ 5310.13: rev-list count with blob:limit=1k 7.29(6.92+0.30)
+ 5310.14: rev-list count with tree:0 0.08(0.03+0.04)
+ 5310.15: simulated partial clone 9.45(8.12+0.41)
+ 5310.19: repack to disk (lookup=true) 255.92(188.13+20.47)
+ 5310.20: simulated clone 13.78(8.84+1.09)
+ 5310.21: simulated fetch 0.52(0.63+0.14)
+ 5310.22: pack to file (bitmap) 44.34(28.94+6.84)
+ 5310.23: rev-list (commits) 0.48(0.31+0.06)
+ 5310.24: rev-list (objects) 4.02(3.93+0.07)
+ 5310.25: rev-list with tag negated via --not 0.04(0.00+0.03)
--all (objects)
- 5310.26: rev-list with negative tag (objects) 0.22(0.16+0.05)
- 5310.27: rev-list count with blob:none 0.22(0.16+0.05)
- 5310.28: rev-list count with blob:limit=1k 6.45(5.87+0.31)
- 5310.29: rev-list count with tree:0 0.22(0.16+0.05)
- 5310.30: simulated partial clone 5.17(3.12+0.48)
+ 5310.26: rev-list with negative tag (objects) 0.04(0.00+0.03)
+ 5310.27: rev-list count with blob:none 0.04(0.01+0.03)
+ 5310.28: rev-list count with blob:limit=1k 6.48(6.23+0.22)
+ 5310.29: rev-list count with tree:0 0.04(0.01+0.03)
+ 5310.30: simulated partial clone 8.30(7.21+0.36)

Test 4-15 are tested without using lookup table. Same tests are repeated in 16-30 (using lookup table).

- Signed-off-by: Abhradeep Chakraborty <[email protected]>

+ Signed-off-by: Abhradeep Chakraborty <[email protected]>

## t/perf/p5310-pack-bitmaps.sh ##
-@@ t/perf/p5310-pack-bitmaps.sh: test_expect_success 'setup bitmap config' '
- git config pack.writebitmaps true
+@@ t/perf/p5310-pack-bitmaps.sh: test_perf_large_repo
+ # We intentionally use the deprecated pack.writebitmaps
+ # config so that we can test against older versions of git.
+ test_expect_success 'setup bitmap config' '
+- git config pack.writebitmaps true
++ git config pack.writebitmaps true &&
++ git config pack.writeReverseIndex true
'

-# we need to create the tag up front such that it is covered by the repack and
@@ t/perf/p5310-pack-bitmaps.sh: test_expect_success 'setup bitmap config' '
-test_expect_success 'create tags' '
- git tag --message="tag pointing to HEAD" perf-tag HEAD
-'
--
++test_bitmap () {
++ local enabled="$1"
+
-test_perf 'repack to disk' '
- git repack -ad
-'
--
++ # we need to create the tag up front such that it is covered by the repack and
++ # thus by generated bitmaps.
++ test_expect_success 'create tags' '
++ git tag --message="tag pointing to HEAD" perf-tag HEAD
++ '
+
-test_full_bitmap
--
++ test_expect_success "use lookup table: $enabled" '
++ git config pack.writeBitmapLookupTable '"$enabled"'
++ '
+
-test_expect_success 'create partial bitmap state' '
- # pick a commit to represent the repo tip in the past
- cutoff=$(git rev-list HEAD~100 -1) &&
- orig_tip=$(git rev-parse HEAD) &&
--
++ test_perf "repack to disk (lookup=$enabled)" '
++ git repack -ad
++ '
+
- # now kill off all of the refs and pretend we had
- # just the one tip
- rm -rf .git/logs .git/refs/* .git/packed-refs &&
- git update-ref HEAD $cutoff &&
--
++ test_full_bitmap
+
- # and then repack, which will leave us with a nice
- # big bitmap pack of the "old" history, and all of
- # the new history will be loose, as if it had been pushed
- # up incrementally and exploded via unpack-objects
- git repack -Ad &&
--
++ test_expect_success "create partial bitmap state (lookup=$enabled)" '
++ # pick a commit to represent the repo tip in the past
++ cutoff=$(git rev-list HEAD~100 -1) &&
++ orig_tip=$(git rev-parse HEAD) &&
+
- # and now restore our original tip, as if the pushes
- # had happened
- git update-ref HEAD $orig_tip
-'
--
--test_partial_bitmap
-+test_bitmap () {
-+ local enabled="$1"
-+
-+ # we need to create the tag up front such that it is covered by the repack and
-+ # thus by generated bitmaps.
-+ test_expect_success 'create tags' '
-+ git tag --message="tag pointing to HEAD" perf-tag HEAD
-+ '
-+
-+ test_expect_success "use lookup table: $enabled" '
-+ git config pack.writeBitmapLookupTable '"$enabled"'
-+ '
-+
-+ test_perf "repack to disk (lookup=$enabled)" '
-+ git repack -ad
-+ '
-+
-+ test_full_bitmap
-+
-+ test_expect_success "create partial bitmap state (lookup=$enabled)" '
-+ # pick a commit to represent the repo tip in the past
-+ cutoff=$(git rev-list HEAD~100 -1) &&
-+ orig_tip=$(git rev-parse HEAD) &&
-+
+ # now kill off all of the refs and pretend we had
+ # just the one tip
+ rm -rf .git/logs .git/refs/* .git/packed-refs &&
@@ t/perf/p5310-pack-bitmaps.sh: test_expect_success 'setup bitmap config' '
+ # and now restore our original tip, as if the pushes
+ # had happened
+ git update-ref HEAD $orig_tip
-+ '
++ '
+}
-+
+
+-test_partial_bitmap
+test_bitmap false
+test_bitmap true

@@ t/perf/p5326-multi-pack-bitmaps.sh: test_description='Tests performance using mi
-
-test_partial_bitmap
+test_bitmap () {
-+ local enabled="$1"
++ local enabled="$1"
+
+ # we need to create the tag up front such that it is covered by the repack and
+ # thus by generated bitmaps.
@@ t/perf/p5326-multi-pack-bitmaps.sh: test_description='Tests performance using mi
+
+ test_full_bitmap
+
-+ test_expect_success "create partial bitmap state (lookup=$enabled)" '
++ test_expect_success "create partial bitmap state (lookup=$enabled)" '
+ # pick a commit to represent the repo tip in the past
+ cutoff=$(git rev-list HEAD~100 -1) &&
+ orig_tip=$(git rev-parse HEAD) &&
@@ t/perf/p5326-multi-pack-bitmaps.sh: test_description='Tests performance using mi
+ # and now restore our original tip, as if the pushes
+ # had happened
+ git update-ref HEAD $orig_tip
-+ '
++ '
+}
+
+test_bitmap false
6: fe556b58814 ! 6: 4f9f1049485 p5310-pack-bitmaps.sh: enable pack.writeReverseIndex for testing
@@ Metadata
Author: Abhradeep Chakraborty <[email protected]>

## Commit message ##
- p5310-pack-bitmaps.sh: enable pack.writeReverseIndex for testing
+ p5310-pack-bitmaps.sh: remove pack.writeReverseIndex

- Enable pack.writeReverseIndex to true to see the effect of writing
- the reverse index in the existing bitmap tests (with and without
- lookup table).
+ The previous change enables the pack.writereverseindex to see
+ the effect of writing reverse index in the performance test.
+
+ Remove the pack.writeReverseIndex configuration.

Below is the result of performance test. Output format is in seconds.

- Test this tree
- -------------------------------------------------------------------
- 5310.4: repack to disk (lookup=false) 294.92(257.60+14.29)
- 5310.5: simulated clone 14.97(8.95+1.31)
- 5310.6: simulated fetch 1.64(2.77+0.20)
- 5310.7: pack to file (bitmap) 41.76(29.33+6.77)
- 5310.8: rev-list (commits) 0.71(0.49+0.09)
- 5310.9: rev-list (objects) 4.65(4.55+0.09)
- 5310.10: rev-list with tag negated via --not 0.08(0.02+0.05)
+ Test this tree
+ ------------------------------------------------------------------------
+ 5310.4: repack to disk (lookup=false) 293.80(251.30+14.30)
+ 5310.5: simulated clone 12.50(5.15+1.36)
+ 5310.6: simulated fetch 1.83(2.90+0.23)
+ 5310.7: pack to file (bitmap) 39.70(20.25+7.14)
+ 5310.8: rev-list (commits) 1.00(0.60+0.13)
+ 5310.9: rev-list (objects) 4.11(4.00+0.10)
+ 5310.10: rev-list with tag negated via --not 0.07(0.02+0.05)
--all (objects)
- 5310.11: rev-list with negative tag (objects) 0.06(0.01+0.04)
- 5310.12: rev-list count with blob:none 0.09(0.03+0.05)
- 5310.13: rev-list count with blob:limit=1k 7.58(7.06+0.33)
- 5310.14: rev-list count with tree:0 0.09(0.03+0.06)
- 5310.15: simulated partial clone 8.64(8.04+0.35)
- 5310.19: repack to disk (lookup=true) 249.86(191.57+19.50)
- 5310.20: simulated clone 13.67(8.83+1.06)
- 5310.21: simulated fetch 0.50(0.63+0.13)
- 5310.22: pack to file (bitmap) 41.24(28.99+6.67)
- 5310.23: rev-list (commits) 0.67(0.50+0.07)
- 5310.24: rev-list (objects) 4.88(4.79+0.08)
- 5310.25: rev-list with tag negated via --not 0.04(0.00+0.03)
+ 5310.11: rev-list with negative tag (objects) 0.23(0.16+0.06)
+ 5310.12: rev-list count with blob:none 0.27(0.18+0.08)
+ 5310.13: rev-list count with blob:limit=1k 6.41(5.98+0.41)
+ 5310.14: rev-list count with tree:0 0.26(0.18+0.07)
+ 5310.15: simulated partial clone 4.34(3.29+0.37)
+ 5310.19: repack to disk (lookup=true) 250.93(171.97+20.78)
+ 5310.20: simulated clone 10.80(5.14+1.06)
+ 5310.21: simulated fetch 0.71(0.79+0.16)
+ 5310.22: pack to file (bitmap) 39.49(20.19+6.98)
+ 5310.23: rev-list (commits) 0.81(0.48+0.09)
+ 5310.24: rev-list (objects) 3.48(3.38+0.09)
+ 5310.25: rev-list with tag negated via --not 0.04(0.00+0.03)
--all (objects)
- 5310.26: rev-list with negative tag (objects) 0.05(0.00+0.04)
- 5310.27: rev-list count with blob:none 0.05(0.01+0.03)
- 5310.28: rev-list count with blob:limit=1k 8.02(7.16+0.34)
- 5310.29: rev-list count with tree:0 0.05(0.01+0.04)
- 5310.30: simulated partial clone 8.57(8.16+0.32)
+ 5310.26: rev-list with negative tag (objects) 0.22(0.16+0.05)
+ 5310.27: rev-list count with blob:none 0.22(0.16+0.05)
+ 5310.28: rev-list count with blob:limit=1k 6.21(5.76+0.29)
+ 5310.29: rev-list count with tree:0 0.23(0.16+0.06)
+ 5310.30: simulated partial clone 4.53(3.14+0.39)

Tests 4-15 are without the use of lookup table. The rests are repeatation of the previous tests but using lookup table.

- Signed-off-by: Abhradeep Chakraborty <[email protected]>

+ Signed-off-by: Abhradeep Chakraborty <[email protected]>

## t/perf/p5310-pack-bitmaps.sh ##
@@ t/perf/p5310-pack-bitmaps.sh: test_perf_large_repo
# We intentionally use the deprecated pack.writebitmaps # config so that we can test against older versions of git. test_expect_success 'setup bitmap config' '
-- git config pack.writebitmaps true
-+ git config pack.writebitmaps true &&
-+ git config pack.writeReverseIndex true
+- git config pack.writebitmaps true &&
+- git config pack.writeReverseIndex true
++ git config pack.writebitmaps true
'

test_bitmap () {

Signature · 1 line
gitgitgadget
#86 lore raw

PATCH v3 3/6 pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

From: Abhradeep Chakraborty <[email protected]>

Teach Git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'. Default is false.

Also add test to verify writting of lookup table.

---
 Documentation/config/pack.txt     |   7 +
 builtin/multi-pack-index.c        |   7 +
 builtin/pack-objects.c            |   8 +
 midx.c                            |   3 +
 midx.h                            |   1 +
 t/t5310-pack-bitmaps.sh           | 792 ++++++++++++++++--------------
 t/t5311-pack-bitmaps-shallow.sh   |  53 +-
 t/t5326-multi-pack-bitmaps.sh     | 421 +++++++++-------
 t/t5327-multi-pack-bitmaps-rev.sh |   9 +
 9 files changed, 720 insertions(+), 581 deletions(-)
Documentation/config/pack.txt +7 -0
index ad7f73a1ead..b955ca572ec 100644
@@ -164,6 +164,13 @@ When writing a multi-pack reachability bitmap, no new namehashes are
 computed; instead, any namehashes stored in an existing bitmap are
 permuted into their appropriate location when writing a new bitmap.
 
+pack.writeBitmapLookupTable::
+	When true, Git will include a "lookup table" section in the
+	bitmap index (if one is written). This table is used to defer
+	loading individual bitmaps as late as possible. This can be
+	beneficial in repositories that have relatively large bitmap
+	indexes. Defaults to false.
+
 pack.writeReverseIndex::
 	When true, git will write a corresponding .rev file (see:
 	link:../technical/pack-format.html[Documentation/technical/pack-format.txt])
builtin/multi-pack-index.c +7 -0
index 5edbb7fe86e..55402b46f41 100644
@@ -87,6 +87,13 @@ static int git_multi_pack_index_write_config(const char *var, const char *value,
 			opts.flags &= ~MIDX_WRITE_BITMAP_HASH_CACHE;
 	}
 
+	if (!strcmp(var, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(var, value))
+			opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+		else
+			opts.flags &= ~MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+	}
+
 	/*
 	 * We should never make a fall-back call to 'git_default_config', since
 	 * this was already called in 'cmd_multi_pack_index()'.
builtin/pack-objects.c +8 -0
index 39e28cfcafc..46e26774963 100644
@@ -3148,6 +3148,14 @@ static int git_pack_config(const char *k, const char *v, void *cb)
 		else
 			write_bitmap_options &= ~BITMAP_OPT_HASH_CACHE;
 	}
+
+	if (!strcmp(k, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(k, v))
+			write_bitmap_options |= BITMAP_OPT_LOOKUP_TABLE;
+		else
+			write_bitmap_options &= ~BITMAP_OPT_LOOKUP_TABLE;
+	}
+
 	if (!strcmp(k, "pack.usebitmaps")) {
 		use_bitmap_index_default = git_config_bool(k, v);
 		return 0;
midx.c +3 -0
index 5f0dd386b02..9c26d04bfde 100644
@@ -1072,6 +1072,9 @@ static int write_midx_bitmap(char *midx_name, unsigned char *midx_hash,
 	if (flags & MIDX_WRITE_BITMAP_HASH_CACHE)
 		options |= BITMAP_OPT_HASH_CACHE;
 
+	if (flags & MIDX_WRITE_BITMAP_LOOKUP_TABLE)
+		options |= BITMAP_OPT_LOOKUP_TABLE;
+
 	prepare_midx_packing_data(&pdata, ctx);
 
 	commits = find_commits_for_midx_bitmap(&commits_nr, refs_snapshot, ctx);
midx.h +1 -0
index 22e8e53288e..5578cd7b835 100644
@@ -47,6 +47,7 @@ struct multi_pack_index {
 #define MIDX_WRITE_REV_INDEX (1 << 1)
 #define MIDX_WRITE_BITMAP (1 << 2)
 #define MIDX_WRITE_BITMAP_HASH_CACHE (1 << 3)
+#define MIDX_WRITE_BITMAP_LOOKUP_TABLE (1 << 4)
 
 const unsigned char *get_midx_checksum(struct multi_pack_index *m);
 void get_midx_filename(struct strbuf *out, const char *object_dir);
t/t5310-pack-bitmaps.sh +410 -382

807 lines not on this page · raw message

t/t5311-pack-bitmaps-shallow.sh +35 -18

58 lines not on this page · raw message

t/t5326-multi-pack-bitmaps.sh +240 -181

513 lines not on this page · raw message

t/t5327-multi-pack-bitmaps-rev.sh +9 -0
index d30ba632c87..d01c61c0c7e 100755
@@ -20,4 +20,13 @@ export GIT_TEST_MIDX_READ_RIDX
 midx_bitmap_core rev
 midx_bitmap_partial_tests rev
 
+test_expect_success 'reinitialize the repository with lookup table enabled' '
+    rm -fr * .git &&
+    git init &&
+    git config pack.writeBitmapLookupTable true
+'
+
+midx_bitmap_core rev
+midx_bitmap_partial_tests rev
+
 test_done
Signature · 1 line
gitgitgadget
#87 lore raw

PATCH v3 4/6 pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Earlier change teaches Git to write bitmap lookup table. But Git does not know how to parse them.

Teach Git to parse the existing bitmap lookup table. The older versions of Git are not affected by it. Those versions ignore the lookup table.

---
 pack-bitmap.c           | 266 ++++++++++++++++++++++++++++++++++++++--
 pack-bitmap.h           |   9 ++
 t/t5310-pack-bitmaps.sh |  22 ++++
 3 files changed, 287 insertions(+), 10 deletions(-)
pack-bitmap.c +256 -10
index 36134222d7a..e22bbbdc60e 100644
@@ -82,6 +82,12 @@ struct bitmap_index {
 	/* The checksum of the packfile or MIDX; points into map. */
 	const unsigned char *checksum;
 
+	/*
+	 * If not NULL, this point into the commit table extension
+	 * (within the memory mapped region `map`).
+	 */
+	unsigned char *table_lookup;
+
 	/*
 	 * Extended index.
 	 *
@@ -185,6 +191,16 @@ static int load_bitmap_header(struct bitmap_index *index)
 			index->hashes = (void *)(index_end - cache_size);
 			index_end -= cache_size;
 		}
+
+		if (flags & BITMAP_OPT_LOOKUP_TABLE) {
+			size_t table_size = st_mult(ntohl(header->entry_count),
+						    BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH);
+			if (table_size > index_end - index->map - header_size)
+				return error(_("corrupted bitmap index file (too short to fit lookup table)"));
+			if (git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1))
+				index->table_lookup = (void *)(index_end - table_size);
+			index_end -= table_size;
+		}
 	}
 
 	index->entry_count = ntohl(header->entry_count);
@@ -211,11 +227,13 @@ static struct stored_bitmap *store_bitmap(struct bitmap_index *index,
 
 	hash_pos = kh_put_oid_map(index->bitmaps, stored->oid, &ret);
 
-	/* a 0 return code means the insertion succeeded with no changes,
-	 * because the SHA1 already existed on the map. this is bad, there
-	 * shouldn't be duplicated commits in the index */
+	/*
+	 * A 0 return code means the insertion succeeded with no changes,
+	 * because the SHA1 already existed on the map. This is bad, there
+	 * shouldn't be duplicated commits in the index.
+	 */
 	if (ret == 0) {
-		error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
+		error(_("duplicate entry in bitmap index: %s"), oid_to_hex(oid));
 		return NULL;
 	}
 
@@ -470,7 +488,7 @@ static int load_bitmap(struct bitmap_index *bitmap_git)
 		!(bitmap_git->tags = read_bitmap_1(bitmap_git)))
 		goto failed;
 
-	if (load_bitmap_entries_v1(bitmap_git) < 0)
+	if (!bitmap_git->table_lookup && load_bitmap_entries_v1(bitmap_git) < 0)
 		goto failed;
 
 	return 0;
@@ -557,13 +575,229 @@ struct include_data {
 	struct bitmap *seen;
 };
 
+struct bitmap_lookup_table_triplet {
+	uint32_t commit_pos;
+	uint64_t offset;
+	uint32_t xor_row;
+};
+
+struct bitmap_lookup_table_xor_item {
+	struct object_id oid;
+	uint64_t offset;
+};
+
+/*
+ * This function gets the raw triplet from `row`'th row in the
+ * lookup table and fills that data to the `triplet`.
+ */
+static int lookup_table_get_triplet(struct bitmap_index *bitmap_git,
+				    uint32_t pos,
+				    struct bitmap_lookup_table_triplet *triplet)
+{
+	unsigned char *p = NULL;
+	if (pos >= bitmap_git->entry_count)
+		return error(_("corrupt bitmap lookup table: triplet position out of index"));
+
+	p = bitmap_git->table_lookup + st_mult(pos, BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH);
+
+	triplet->commit_pos = get_be32(p);
+	p += sizeof(uint32_t);
+	triplet->offset = get_be64(p);
+	p += sizeof(uint64_t);
+	triplet->xor_row = get_be32(p);
+	return 0;
+}
+
+/*
+ * Searches for a matching triplet. `va` is a pointer
+ * to the wanted commit position value. `vb` points to
+ * a triplet in lookup table. The first 4 bytes of each
+ * triplet (pointed by `vb`) are compared with `*va`.
+ */
+static int triplet_cmp(const void *va, const void *vb)
+{
+
+	uint32_t a = *(uint32_t *)va;
+	uint32_t b = get_be32(vb);
+	if (a > b)
+		return 1;
+	else if (a < b)
+		return -1;
+
+	return 0;
+}
+
+static uint32_t bsearch_pos(struct bitmap_index *bitmap_git,
+			    struct object_id *oid,
+			    uint32_t *result)
+{
+	int found;
+
+	if (bitmap_is_midx(bitmap_git))
+		found = bsearch_midx(oid, bitmap_git->midx, result);
+	else
+		found = bsearch_pack(oid, bitmap_git->pack, result);
+
+	return found;
+}
+
+/*
+ * `bsearch_triplet` function searches for the raw triplet having
+ * commit position same as `commit_pos` and fills `triplet`
+ * object from the raw triplet. Returns 1 on success and 0
+ * on failure.
+ */
+static int bsearch_triplet(uint32_t *commit_pos,
+			   struct bitmap_index *bitmap_git,
+			   struct bitmap_lookup_table_triplet *triplet)
+{
+	unsigned char *p = bsearch(commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+				   BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH, triplet_cmp);
+
+	if (!p)
+		return 0;
+	triplet->commit_pos = get_be32(p);
+	p += sizeof(uint32_t);
+	triplet->offset = get_be64(p);
+	p += sizeof(uint64_t);
+	triplet->xor_row = get_be32(p);
+	return 1;
+}
+
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+					  struct commit *commit)
+{
+	uint32_t commit_pos, xor_row;
+	uint64_t offset;
+	int flags;
+	struct bitmap_lookup_table_triplet triplet;
+	struct object_id *oid = &commit->object.oid;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap = NULL;
+
+	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
+
+	if (!found)
+		return NULL;
+
+	if (!bsearch_triplet(&commit_pos, bitmap_git, &triplet))
+		return NULL;
+
+	offset = triplet.offset;
+	xor_row = triplet.xor_row;
+
+	if (xor_row != 0xffffffff) {
+		int xor_flags;
+		khiter_t hash_pos;
+		uint64_t offset_xor;
+		struct bitmap_lookup_table_xor_item *xor_items;
+		struct bitmap_lookup_table_xor_item xor_item;
+		size_t xor_items_nr = 0, xor_items_alloc = 64;
+
+		ALLOC_ARRAY(xor_items, xor_items_alloc);
+		while (xor_row != 0xffffffff) {
+			struct object_id xor_oid;
+
+			if (xor_items_nr + 1 >= bitmap_git->entry_count) {
+				free(xor_items);
+				error(_("corrupt bitmap lookup table: xor chain exceed entry count"));
+				return NULL;
+			}
+
+			if (lookup_table_get_triplet(bitmap_git, xor_row, &triplet) < 0)
+				return NULL;
+
+			offset_xor = triplet.offset;
+
+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, triplet.commit_pos) < 0) {
+				free(xor_items);
+				error(_("corrupt bitmap lookup table: commit index %u out of range"),
+					triplet.commit_pos);
+				return NULL;
+			}
+
+			hash_pos = kh_get_oid_map(bitmap_git->bitmaps, xor_oid);
+
+			/*
+			 * If desired bitmap is already stored, we don't need
+			 * to iterate further. Because we know that bitmaps
+			 * that are needed to be parsed to parse this bitmap
+			 * has already been stored. So, assign this stored bitmap
+			 * to the xor_bitmap.
+			 */
+			if (hash_pos < kh_end(bitmap_git->bitmaps) &&
+			    (xor_bitmap = kh_value(bitmap_git->bitmaps, hash_pos)))
+				break;
+
+			ALLOC_GROW(xor_items, xor_items_nr + 1, xor_items_alloc);
+			xor_items[xor_items_nr++] = (struct bitmap_lookup_table_xor_item) {.oid = xor_oid,
+											   .offset = offset_xor};
+			xor_row = triplet.xor_row;
+		}
+
+		while (xor_items_nr) {
+			xor_item = xor_items[xor_items_nr - 1];
+			offset_xor = xor_item.offset;
+
+			bitmap_git->map_pos = offset_xor;
+			if (bitmap_git->map_size - bitmap_git->map_pos < 6) {
+				error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
+					oid_to_hex(&xor_item.oid));
+				free(xor_items);
+				return NULL;
+			}
+
+			bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
+			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+			bitmap = read_bitmap_1(bitmap_git);
+
+			if (!bitmap) {
+				free(xor_items);
+				return NULL;
+			}
+
+			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_item.oid, xor_bitmap, xor_flags);
+			xor_items_nr--;
+		}
+
+		free(xor_items);
+	}
+
+	bitmap_git->map_pos = offset;
+	if (bitmap_git->map_size - bitmap_git->map_pos < 6) {
+		error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
+			oid_to_hex(oid));
+		return NULL;
+	}
+
+	bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
+	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+	bitmap = read_bitmap_1(bitmap_git);
+
+	if (!bitmap)
+		return NULL;
+
+	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);
+}
+
 struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
 				      struct commit *commit)
 {
 	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
 					   commit->object.oid);
-	if (hash_pos >= kh_end(bitmap_git->bitmaps))
-		return NULL;
+	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
+		struct stored_bitmap *bitmap = NULL;
+		if (!bitmap_git->table_lookup)
+			return NULL;
+
+		trace2_region_enter("pack-bitmap", "reading_lookup_table", the_repository);
+		/* NEEDSWORK: cache misses aren't recorded */
+		bitmap = lazy_bitmap_for_commit(bitmap_git, commit);
+		trace2_region_leave("pack-bitmap", "reading_lookup_table", the_repository);
+		if (!bitmap)
+			return NULL;
+		return lookup_stored_bitmap(bitmap);
+	}
 	return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos));
 }
 
@@ -1699,8 +1933,10 @@ void test_bitmap_walk(struct rev_info *revs)
 	if (revs->pending.nr != 1)
 		die("you must specify exactly one commit to test");
 
-	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
-		bitmap_git->version, bitmap_git->entry_count);
+	fprintf(stderr, "Bitmap v%d test (%d entries%s)",
+		bitmap_git->version,
+		bitmap_git->entry_count,
+		bitmap_git->table_lookup ? "" : " loaded");
 
 	root = revs->pending.objects[0].item;
 	bm = bitmap_for_commit(bitmap_git, (struct commit *)root);
@@ -1753,13 +1989,23 @@ void test_bitmap_walk(struct rev_info *revs)
 
 int test_bitmap_commits(struct repository *r)
 {
-	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
 	struct object_id oid;
 	MAYBE_UNUSED void *value;
+	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
+
+	/*
+	 * As this function is only used to print bitmap selected
+	 * commits, we don't have to read the commit table.
+	 */
 
 	if (!bitmap_git)
 		die("failed to load bitmap indexes");
 
+	if (bitmap_git->table_lookup) {
+		if (load_bitmap_entries_v1(bitmap_git) < 0)
+			die(_("failed to load bitmap indexes"));
+	}
+
 	kh_foreach(bitmap_git->bitmaps, oid, value, {
 		printf("%s\n", oid_to_hex(&oid));
 	});
pack-bitmap.h +9 -0
index 67a9d0fc303..9278f71ac91 100644
@@ -23,6 +23,15 @@ struct bitmap_disk_header {
 
 #define NEEDS_BITMAP (1u<<22)
 
+/*
+ * The width in bytes of a single triplet in the lookup table
+ * extension:
+ *     (commit_pos, offset, xor_row)
+ *
+ * whose fields ar 32-, 64-, 32- bits wide, respectively.
+ */
+#define BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH (16)
+
 enum pack_bitmap_opts {
 	BITMAP_OPT_FULL_DAG = 0x1,
 	BITMAP_OPT_HASH_CACHE = 0x4,
t/t5310-pack-bitmaps.sh +22 -0
index c0607172827..7e50f8e7653 100755
@@ -258,6 +258,7 @@ test_bitmap_cases () {
 
 	test_expect_success 'truncated bitmap fails gracefully (ewah)' '
 		test_config pack.writebitmaphashcache false &&
+		test_config pack.writebitmaplookuptable false &&
 		git repack -ad &&
 		git rev-list --use-bitmap-index --count --all >expect &&
 		bitmap=$(ls .git/objects/pack/*.bitmap) &&
@@ -270,6 +271,7 @@ test_bitmap_cases () {
 	'
 
 	test_expect_success 'truncated bitmap fails gracefully (cache)' '
+		git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
 		git repack -ad &&
 		git rev-list --use-bitmap-index --count --all >expect &&
 		bitmap=$(ls .git/objects/pack/*.bitmap) &&
@@ -453,4 +455,24 @@ test_expect_success 'verify writing bitmap lookup table when enabled' '
 	grep "\"label\":\"writing_lookup_table\"" trace2
 '
 
+test_expect_success 'lookup table is actually used to traverse objects' '
+	git repack -adb &&
+	GIT_TRACE2_EVENT="$(pwd)/trace3" \
+		git rev-list --use-bitmap-index --count --all &&
+	grep "\"label\":\"reading_lookup_table\"" trace3
+'
+
+test_expect_success 'truncated bitmap fails gracefully (lookup table)' '
+	test_config pack.writebitmaphashcache false &&
+	git repack -adb &&
+	git rev-list --use-bitmap-index --count --all >expect &&
+	bitmap=$(ls .git/objects/pack/*.bitmap) &&
+	test_when_finished "rm -f $bitmap" &&
+	test_copy_bytes 512 <$bitmap >$bitmap.tmp &&
+	mv -f $bitmap.tmp $bitmap &&
+	git rev-list --use-bitmap-index --count --all >actual 2>stderr &&
+	test_cmp expect actual &&
+	test_i18ngrep corrupted.bitmap.index stderr
+'
+
 test_done
Signature · 1 line
gitgitgadget
#95 lore raw

PATCH v3 5/6 bitmap-lookup-table: add performance tests for lookup table

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add performance tests to verify the performance of lookup table with pack.writeReverseIndex enabled. This is to check the performance when the above configuration is set.

Lookup table makes Git run faster in most of the cases. Below is the result of t/perf/p5310-pack-bitmaps.sh.perf/p5326-multi-pack-bitmaps.sh gives similar result. The repository used in the test is linux kernel.

Test this tree
---------------------------------------------------------------------------

5310.4: repack to disk (lookup=false)                   296.55(256.53+14.52)
5310.5: simulated clone                                 15.64(8.88+1.39)
5310.6: simulated fetch                                 1.65(2.75+0.20)
5310.7: pack to file (bitmap)                           48.71(30.20+7.58)
5310.8: rev-list (commits)                              0.61(0.41+0.08)
5310.9: rev-list (objects)                              4.38(4.26+0.09)
5310.10: rev-list with tag negated via --not            0.07(0.02+0.04)
         --all (objects)
5310.11: rev-list with negative tag (objects)           0.05(0.01+0.03)
5310.12: rev-list count with blob:none                  0.08(0.03+0.04)
5310.13: rev-list count with blob:limit=1k              7.29(6.92+0.30)
5310.14: rev-list count with tree:0                     0.08(0.03+0.04)
5310.15: simulated partial clone                        9.45(8.12+0.41)
5310.19: repack to disk (lookup=true)                   255.92(188.13+20.47)
5310.20: simulated clone                                13.78(8.84+1.09)
5310.21: simulated fetch                                0.52(0.63+0.14)
5310.22: pack to file (bitmap)                          44.34(28.94+6.84)
5310.23: rev-list (commits)                             0.48(0.31+0.06)
5310.24: rev-list (objects)                             4.02(3.93+0.07)
5310.25: rev-list with tag negated via --not            0.04(0.00+0.03)
         --all (objects)
5310.26: rev-list with negative tag (objects)           0.04(0.00+0.03)
5310.27: rev-list count with blob:none                  0.04(0.01+0.03)
5310.28: rev-list count with blob:limit=1k              6.48(6.23+0.22)
5310.29: rev-list count with tree:0                     0.04(0.01+0.03)
5310.30: simulated partial clone                        8.30(7.21+0.36)

Test 4-15 are tested without using lookup table. Same tests are repeated in 16-30 (using lookup table).

---
 t/perf/p5310-pack-bitmaps.sh       | 66 ++++++++++++---------
 t/perf/p5326-multi-pack-bitmaps.sh | 93 ++++++++++++++++--------------
 2 files changed, 89 insertions(+), 70 deletions(-)
t/perf/p5310-pack-bitmaps.sh +38 -28
index 7ad4f237bc3..1ad3c3f14c6 100755
@@ -13,42 +13,52 @@ test_perf_large_repo
 # We intentionally use the deprecated pack.writebitmaps
 # config so that we can test against older versions of git.
 test_expect_success 'setup bitmap config' '
-	git config pack.writebitmaps true
+	git config pack.writebitmaps true &&
+	git config pack.writeReverseIndex true
 '
 
-# we need to create the tag up front such that it is covered by the repack and
-# thus by generated bitmaps.
-test_expect_success 'create tags' '
-	git tag --message="tag pointing to HEAD" perf-tag HEAD
-'
+test_bitmap () {
+	local enabled="$1"
 
-test_perf 'repack to disk' '
-	git repack -ad
-'
+	# we need to create the tag up front such that it is covered by the repack and
+	# thus by generated bitmaps.
+	test_expect_success 'create tags' '
+		git tag --message="tag pointing to HEAD" perf-tag HEAD
+	'
 
-test_full_bitmap
+	test_expect_success "use lookup table: $enabled" '
+		git config pack.writeBitmapLookupTable '"$enabled"'
+	'
 
-test_expect_success 'create partial bitmap state' '
-	# pick a commit to represent the repo tip in the past
-	cutoff=$(git rev-list HEAD~100 -1) &&
-	orig_tip=$(git rev-parse HEAD) &&
+	test_perf "repack to disk (lookup=$enabled)" '
+		git repack -ad
+	'
 
-	# now kill off all of the refs and pretend we had
-	# just the one tip
-	rm -rf .git/logs .git/refs/* .git/packed-refs &&
-	git update-ref HEAD $cutoff &&
+	test_full_bitmap
 
-	# and then repack, which will leave us with a nice
-	# big bitmap pack of the "old" history, and all of
-	# the new history will be loose, as if it had been pushed
-	# up incrementally and exploded via unpack-objects
-	git repack -Ad &&
+	test_expect_success "create partial bitmap state (lookup=$enabled)" '
+		# pick a commit to represent the repo tip in the past
+		cutoff=$(git rev-list HEAD~100 -1) &&
+		orig_tip=$(git rev-parse HEAD) &&
 
-	# and now restore our original tip, as if the pushes
-	# had happened
-	git update-ref HEAD $orig_tip
-'
+		# now kill off all of the refs and pretend we had
+		# just the one tip
+		rm -rf .git/logs .git/refs/* .git/packed-refs &&
+		git update-ref HEAD $cutoff &&
+
+		# and then repack, which will leave us with a nice
+		# big bitmap pack of the "old" history, and all of
+		# the new history will be loose, as if it had been pushed
+		# up incrementally and exploded via unpack-objects
+		git repack -Ad &&
+
+		# and now restore our original tip, as if the pushes
+		# had happened
+		git update-ref HEAD $orig_tip
+	'
+}
 
-test_partial_bitmap
+test_bitmap false
+test_bitmap true
 
 test_done
t/perf/p5326-multi-pack-bitmaps.sh +51 -42

98 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#98 lore raw

PATCH v3 6/6 p5310-pack-bitmaps.sh: remove pack.writeReverseIndex

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The previous change enables the pack.writereverseindex to see the effect of writing reverse index in the performance test.

Remove the pack.writeReverseIndex configuration.

Below is the result of performance test. Output format is in seconds.

Test this tree
------------------------------------------------------------------------

5310.4: repack to disk (lookup=false)               293.80(251.30+14.30)
5310.5: simulated clone                             12.50(5.15+1.36)
5310.6: simulated fetch                             1.83(2.90+0.23)
5310.7: pack to file (bitmap)                       39.70(20.25+7.14)
5310.8: rev-list (commits)                          1.00(0.60+0.13)
5310.9: rev-list (objects)                          4.11(4.00+0.10)
5310.10: rev-list with tag negated via --not        0.07(0.02+0.05)
         --all (objects)
5310.11: rev-list with negative tag (objects)       0.23(0.16+0.06)
5310.12: rev-list count with blob:none              0.27(0.18+0.08)
5310.13: rev-list count with blob:limit=1k          6.41(5.98+0.41)
5310.14: rev-list count with tree:0                 0.26(0.18+0.07)
5310.15: simulated partial clone                    4.34(3.29+0.37)
5310.19: repack to disk (lookup=true)               250.93(171.97+20.78)
5310.20: simulated clone                            10.80(5.14+1.06)
5310.21: simulated fetch                            0.71(0.79+0.16)
5310.22: pack to file (bitmap)                      39.49(20.19+6.98)
5310.23: rev-list (commits)                         0.81(0.48+0.09)
5310.24: rev-list (objects)                         3.48(3.38+0.09)
5310.25: rev-list with tag negated via --not        0.04(0.00+0.03)
         --all (objects)
5310.26: rev-list with negative tag (objects)       0.22(0.16+0.05)
5310.27: rev-list count with blob:none              0.22(0.16+0.05)
5310.28: rev-list count with blob:limit=1k          6.21(5.76+0.29)
5310.29: rev-list count with tree:0                 0.23(0.16+0.06)
5310.30: simulated partial clone                    4.53(3.14+0.39)

Tests 4-15 are without the use of lookup table. The rests are repeatation of the previous tests but using lookup table.

---
 t/perf/p5310-pack-bitmaps.sh | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
t/perf/p5310-pack-bitmaps.sh +1 -2
index 1ad3c3f14c6..ac5b7341e8e 100755
@@ -13,8 +13,7 @@ test_perf_large_repo
 # We intentionally use the deprecated pack.writebitmaps
 # config so that we can test against older versions of git.
 test_expect_success 'setup bitmap config' '
-	git config pack.writebitmaps true &&
-	git config pack.writeReverseIndex true
+	git config pack.writebitmaps true
 '
 
 test_bitmap () {
Signature · 1 line
gitgitgadget
#99 lore raw
In reply to Abhradeep Chakraborty via GitGitGadget

Oops, I forgot to edit the PR cover letter!

I am adding it here. Sorry for that :P

Changes since v2:

* Log messages related issues are fixed.
* pack.writeBitmapLookupTable is now by default disabled.
* Documentations are improved.
* xor_row is used instead of xor_pos in triplets.
* In pack-bitmap-write.c, off_t * is used for offsets array (Instead of uint64_t *).
* struct bitmap_lookup_table_triplet is introduced and functions Like triplet_get_offset() and triplet_get_xor_pos() are removed.
* table_size is getting subtracted from index_end irrespective of the value of GIT_TEST_READ_COMMIT_TABLE.
* xor stack filling loop will stop iterating if a xor bitmap is already stored/parsed.
* The stack will now store bitmap_lookup_table_xor_item items Of plain xor_row.
* bitmap related test files are reformatted to allow repeating of tests with bitmap extension enabled.
* comments are added.

Thanks :)

#100 lore raw
In reply to Abhradeep Chakraborty via GitGitGadget

"Abhradeep Chakraborty via GitGitGadget" <[email protected]>

Quoted 9 lines · writes:

When parsing the .bitmap file, git loads all the bitmaps one by one even if some of the bitmaps are not necessary. We can remove this overhead by loading only the necessary bitmaps. A look up table extension can solve this issue.

Changes since v1:

This is the second version which addressed all (I think) the reviews. Please notify me if some reviews are not addressed :)

Is this the second version that is labeled as "v3" ;-)?

Quoted 1 line
 Documentation/technical/bitmap-format.txt |  39 ++

I haven't tried merging it yet, but doesn't [1/6] overlap with and semantically depend on your other series that touch the formatting of this file?

Thanks.

#101 lore raw
Quoted 4 lines · Junio C Hamano
Quoted 2 lines

This is the second version which addressed all (I think) the reviews. Please notify me if some reviews are not addressed :)

Is this the second version that is labeled as "v3" ;-)?

Hi junio,

No, it is actually the third version. I forgot to update the cover letter :P.

I am using Github's gitgitgadget to submit PRs and it uses PR description as the cover letter.

So before submitting a new version of patchset, PR description must be updated which I missed this time.

I wrote a reply comment[1] where you can find a summary of all the new changes.

[1] https://lore.kernel.org/git/[email protected]/

Quoted 5 lines
Quoted 1 line
 Documentation/technical/bitmap-format.txt |  39 ++

I haven't tried merging it yet, but doesn't [1/6] overlap with and semantically depend on your other series that touch the formatting of this file?

Correct, [1/6] indeed depends on my previous patch series[2] and it is assuming that that series has already been merged. As far as it seems, it will not create any merge conflicts while merging but I am not sure. This would be interesting to see.

[2] https://lore.kernel.org/git/[email protected]/

Thanks :)

#102 lore raw
Quoted 11 lines · Abhradeep Chakraborty
Quoted 5 lines · Junio C Hamano
Quoted 1 line
 Documentation/technical/bitmap-format.txt |  39 ++

I haven't tried merging it yet, but doesn't [1/6] overlap with and semantically depend on your other series that touch the formatting of this file?

Correct, [1/6] indeed depends on my previous patch series[2] and it is assuming that that series has already been merged.

I suppose it's the opposite. A quick check shows that the patch applies cleanly over 'master' but fails to apply over 'next' which has the changes from your other patch series. So, the base branch for [1/6] is 'master'. The other 5 patches clearly don't conflict.

Quoted 4 lines

As far as it seems,
it will not create any merge conflicts while merging but I am not sure. This would be interesting to see.

Since the first hunk of 1/6 and your other series touch the same area of Documentation/technical/bitmap-format.txt, the changes conflict. Junio might be able to handle this one. If not, you would need to look into separate 1/6 and based it over your other series to avoid the conflict.

--
Sivaraam

#103 lore raw
Quoted 7 lines · Kaartic Sivaraam
Quoted 2 lines

Correct, [1/6] indeed depends on my previous patch series[2] and it is assuming that that series has already been merged.

I suppose it's the opposite. A quick check shows that the patch applies cleanly over 'master' but fails to apply over 'next' which has the changes from your other patch series. So, the base branch for [1/6] is 'master'. The other 5 patches clearly don't conflict.

Actually by saying "[1/6] indeed depends on my previous patch series[2] and it is assuming that that series has already been merged.", I wanted to mean that the format followed in this patch (e.g. description list, indentation etc.) is dependent on the format changes introduced in that Patch series.

If you say about the base branch, yes, you're right. The base branch is 'Master'.

Quoted 5 lines

Since the first hunk of 1/6 and your other series touch the same area of Documentation/technical/bitmap-format.txt, the changes conflict. Junio might be able to handle this one. If not, you would need to look into separate 1/6 and based it over your other series to avoid the conflict.

Oh, I see. I have no problem doing that :)
Let me know if Junio face any problem fixing the conflict.

Thanks :)

#72 lore raw

Re: [PATCH v3 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

Hi Abhradeep,

Quoted 9 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, Git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx or pack, along with their offset and xor offset. This way git can load only the necessary bitmaps without loading the previous bitmaps.

Quoted 5 lines

Older versions of Git ignore the lookup table extension and don't throw any kind of warning or error while parsing the bitmap file.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

Not sure if this is new in this extension, but should there be a link or two to the basics of XOR compression and some of the bitmap look up techniques?

It's not always obvious if these techniques are 'heuristic' and only have partial commit data, or they have all the commits listed, Nor how/why they work. My point is more about giving new readers a hand-up in their understanding, rather than simple implementation details for those who already know what is going on. For example, are there any external articles that you found helpful in getting started that could be referenced somewhere in the docs?

Separately I'm preparing a short series on adding 'reachability bitmap' and 'commit graph' (among other stuff) to the glossary as part of giving folks [0] stepping stones to cross the chasm of understanding

Philip

[0] me included;-)

Quoted 63 lines
---
 Documentation/technical/bitmap-format.txt | 39 +++++++++++++++++++++++
 1 file changed, 39 insertions(+)
Documentation/technical/bitmap-format.txt +39 -0
index 04b3ec21785..c30dc177643 100644
@@ -67,6 +67,17 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` <commit_pos, offset, xor_row>
+			triplets. The format and meaning of the table is described
+			below.
++
+NOTE: Unlike the xor_offset used to compress an individual bitmap,
+`xor_row` stores an *absolute* index into the lookup table, not a location
+relative to the current entry.
+
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +216,31 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last `N * (4 + 8 + 4)`
+bytes (preceding the name-hash cache and trailing hash) of the `.bitmap`
+file contains a lookup table specifying the information needed to get
+the desired bitmap from the entries without parsing previous unnecessary
+bitmaps.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the table
+contains a list of `nr_entries` <commit_pos, offset, xor_row> triplets
+(sorted in the ascending order of `commit_pos`). The content of i'th
+triplet is -
+
+	* {empty}
+	commit_pos (4 byte integer, network byte order): ::
+	It stores the object position of a commit (in the midx or pack
+	index).
+
+	* {empty}
+	offset (8 byte integer, network byte order): ::
+	The offset from which that commit's bitmap can be read.
+
+	* {empty}
+	xor_row (4 byte integer, network byte order): ::
+	The position of the triplet whose bitmap is used to compress
+	this one, or `0xffffffff` if no such bitmap exists.
#73 lore raw

Re: [PATCH v3 1/6] Documentation/technical: describe bitmap lookup table extension

Hello Philip,

Quoted 11 lines · Philip Oakley

Not sure if this is new in this extension, but should there be a link or two to the basics of XOR compression and some of the bitmap look up techniques?

It's not always obvious if these techniques are 'heuristic' and only have partial commit data, or they have all the commits listed, Nor how/why they work. My point is more about giving new readers a hand-up in their understanding, rather than simple implementation details for those who already know what is going on. For example, are there any external articles that you found helpful in getting started that could be referenced somewhere in the docs?

As this series is only about adding a lookup-table extension (and not about bitmap itself), I am not sure whether it's good to include those things in this series. But I agree with your point that it should be able build a logical understanding among the new readers.

There are some external articles[1] which talk about bitmap internals. But I think it would be better if we can make a new doc file (may be Documentation/technical/reachability-bitmaps.txt or similar) rather than putting those details in the bitmap-format.txt (As the name suggests, this file should only contain format details of bitmaps). That file would provide the answers of "Why bitmaps", "how they are stored", "How they are fetched", "how they work with pack-objects, git-fetch, midx etc.", "Detailed explanation of each bitmap extension" , and lastly "what are the future works" (if any).

What do you think?

Quoted 3 lines

Separately I'm preparing a short series on adding 'reachability bitmap' and 'commit graph' (among other stuff) to the glossary as part of giving folks [0] stepping stones to cross the chasm of understanding

Great!

Thanks :)

[1] https://github.blog/2015-09-22-counting-objects/, https://github.blog/2021-04-29-scaling-monorepo-maintenance/

#74 lore raw

Re: [PATCH v3 1/6] Documentation/technical: describe bitmap lookup table extension

Quoted 15 lines · Abhradeep Chakraborty

Hello Philip,

Quoted 11 lines · Philip Oakley

Not sure if this is new in this extension, but should there be a link or two to the basics of XOR compression and some of the bitmap look up techniques?

It's not always obvious if these techniques are 'heuristic' and only have partial commit data, or they have all the commits listed, Nor how/why they work. My point is more about giving new readers a hand-up in their understanding, rather than simple implementation details for those who already know what is going on. For example, are there any external articles that you found helpful in getting started that could be referenced somewhere in the docs?

Quoted 3 lines

As this series is only about adding a lookup-table extension (and not about bitmap itself), I am not sure whether it's good to include those things in this series.

Thanks for the clarification. I must have slight misread some of the discussions and falsely thought it was the XOR compression (which is a technique I wasn't really aware of), that was being provided by the extension - Where would it be best for me to look up the background to your "extension" project?

Quoted 2 lines

But I agree with your point that it should be able build a logical understanding among the new readers.

*nod*

Quoted 5 lines

There are some external articles[1] which talk about bitmap internals. But I think it would be better if we can make a new doc file (may be Documentation/technical/reachability-bitmaps.txt or similar) rather than putting those details in the bitmap-format.txt

Thanks for the two links. In general I agree about the format document.

Quoted 6 lines

(As the name
suggests, this file should only contain format details of bitmaps). That file would provide the answers of "Why bitmaps", "how they are stored", "How they are fetched", "how they work with pack-objects, git-fetch, midx etc.", "Detailed explanation of each bitmap extension" , and lastly "what are the future works" (if any).

One thing I've realised on reflection is that I'm unclear how the 'reachability bitmaps' and the 'commit-graph file' techniques relate to each other (and to the ODB DAG), and what features they pick out within their heuristic, explained at just enough level to allow folks to appreciate what the options that select them will do for their use case.

Quoted 2 lines

What do you think?

I'd be happy to collate contributions, suggestions and thoughts.

Trying to create these good introductory descriptions can be really difficult, as you can only step into the same river once (the 'reading for the first time problem' of not being able to un-hear the explanations of others when reading a 2nd draft...)

Quoted 9 lines
Quoted 4 lines

Separately I'm preparing a short series on adding 'reachability bitmap' and 'commit graph' (among other stuff) to the glossary as part of giving folks [0] stepping stones to cross the chasm of understanding

Great!

Thanks :)

[1] https://github.blog/2015-09-22-counting-objects/, https://github.blog/2021-04-29-scaling-monorepo-maintenance/

Thank you.

#75 lore raw

Re: [PATCH v3 1/6] Documentation/technical: describe bitmap lookup table extension

Quoted 15 lines · Philip Oakley
Quoted 11 lines
Quoted 11 lines

Not sure if this is new in this extension, but should there be a link or two to the basics of XOR compression and some of the bitmap look up techniques?

It's not always obvious if these techniques are 'heuristic' and only have partial commit data, or they have all the commits listed, Nor how/why they work. My point is more about giving new readers a hand-up in their understanding, rather than simple implementation details for those who already know what is going on. For example, are there any external articles that you found helpful in getting started that could be referenced somewhere in the docs?

Quoted 3 lines

As this series is only about adding a lookup-table extension (and not about bitmap itself), I am not sure whether it's good to include those things in this series.

Quoted 5 lines

Thanks for the clarification. I must have slight misread some of the discussions and falsely thought it was the XOR compression (which is a technique I wasn't really aware of), that was being provided by the extension - Where would it be best for me to look up the background to your "extension" project?

Yeah, Abhradeep is right that the XOR compression isn't new, we already serialize bitmaps with optional XOR offsets. The gist is that we give an offset of some previous bitmap that is used to compress the current one by XORing the bits in the current bitamp with the previous one. These XOR-compressed bitmaps are often sparse, so they compress well and reduce the overall size of the .bitmap.

A slightly more detailed overview can be found in Documentation/technical/bitmap-format.txt under the bullet point reading "1-byte XOR-offset".

Thanks,
Taylor

#79 lore raw

Re: [PATCH v3 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 4 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documented by an earlier change, but Git does not yet know how to write that extension.

This and the first patch both look in great shape to me. I haven't had a chance to take a close look through the remaining four patches, but I anticipate that they are in similarly-good shape.

I'll have some more time to finish reviewing this tomorrow morning. I want to give it a closer inspection this round to make sure that everything is correct (and that we're assembling the various orderings the right way by stepping through it in a debugger, etc., etc.).

Thanks for all of your patience :-).

Thanks,
Taylor

#80 lore raw

Re: [PATCH v3 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

Hi Abhradeep,

I just wanted to make absolutely sure that I understood what the implementation in this patch was doing, since I think generating and converting between all of these different orderings is by far the most confusing component of this series.

Quoted 7 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documented by an earlier change, but Git does not yet know how to write that extension.

pack-bitmap-write.c +3 -0
+static int table_cmp(const void *_va, const void *_vb, void *_data)
+{
+	uint32_t *commit_positions = _data;

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 697

Quoted 10 lines
pack-bitmap-write.c +10 -0
+	uint32_t a = commit_positions[*(uint32_t *)_va];
+	uint32_t b = commit_positions[*(uint32_t *)_vb];
+
+	if (a > b)
+		return 1;
+	else if (a < b)
+		return -1;
+
+	return 0;
+}

Let's skip the above part for now, and just look at the implementation of writing_lookup_table():

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 709

Quoted 11 lines
pack-bitmap-write.c +11 -0
+static void write_lookup_table(struct hashfile *f,
+			       struct pack_idx_entry **index,
+			       uint32_t index_nr,
+			       off_t *offsets)
+{
+	uint32_t i;
+	uint32_t *table, *table_inv, *commit_positions;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+	ALLOC_ARRAY(commit_positions, writer.selected_nr);

Makes sense.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 719

Quoted 9 lines
pack-bitmap-write.c +9 -0
+	/* store the index positions of the commits */
+	for (i = 0; i < writer.selected_nr; i++) {
+		int pos = commit_bitmap_writer_pos(&writer.selected[i].commit->object.oid,
+						   index, index_nr);
+		if (pos < 0)
+			BUG(_("trying to write commit not in index"));
+
+		commit_positions[i] = pos;
+	}

By the end of this loop, we have an array commit_positions which maps the ith selected commit to its lexical position among all objects in the bitmap. IOW, commit_positions[i] = j means the ith selected commit can be found at index j among all objects in the pack/MIDX in their lexical order.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 722

Quoted 2 lines
pack-bitmap-write.c +2 -0
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;

At this point, table[i] = i.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 729

Quoted 6 lines
pack-bitmap-write.c +6 -0
+	/*
+	 * At the end of this sort table[j] = i means that the i'th
+	 * bitmap corresponds to j'th bitmapped commit in lex order of
+	 * OIDs.
+	 */
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);

And then we sort table by treating its values as indexes into commit_positions. Here's where I'm not sure that I follow what's going on. You say above that table[j] = i, where i corresponds to the order of selected commits, and j is in lexical order.

If that's the case, then I'd expect that printing index[table[j]] for increasing j would output OIDs in increasing lexical order. But that doesn't quite seem to be the case. From a debugger session that has a breakpoint after computing and sorting table, along with building table_inv:

    (gdb) p oid_to_hex(&index[table[0]]->oid)
    $17 = 0x555555983ea0 <hexbuffer> "0006763074748d43b539c1c8e8882c08034ab178"
    (gdb) p oid_to_hex(&index[table[1]]->oid)
    $18 = 0x555555983ee1 <hexbuffer+65> "001ce83dd43f03dcfc67f29d38922e4a9682aab0"
    (gdb) p oid_to_hex(&index[table[2]]->oid)
    $19 = 0x555555983f22 <hexbuffer+130> "002db882ece2ab6a240e495a169c6e06422289c8"
    (gdb) p oid_to_hex(&index[table[3]]->oid)
    $20 = 0x555555983f63 <hexbuffer+195> "0007a5feb040e1ff704f3ad636619ddca3e7382b"

that doesn't look like the OIDs are increasing in lexical order.

I'm not quite sure if I'm even looking at the right thing, or if this is to be expected, or if the comment isn't quite accurate. If you could help clarify what's going on here, that would be great.

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 735

Quoted 5 lines
pack-bitmap-write.c +5 -0
+	/* table_inv helps us discover that relationship (i'th bitmap
+	 * to j'th commit by j = table_inv[i])
+	 */
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;

This part makes sense, as does the rest of the implementation.

Thanks,
Taylor

#88 lore raw

Re: [PATCH v3 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

pack-bitmap.c struct include_data { · line 620

Quoted 10 lines · Abhradeep Chakraborty via GitGitGadget
pack-bitmap.c +10 -0
+/*
+ * Searches for a matching triplet. `va` is a pointer
+ * to the wanted commit position value. `vb` points to
+ * a triplet in lookup table. The first 4 bytes of each
+ * triplet (pointed by `vb`) are compared with `*va`.
+ */
+static int triplet_cmp(const void *va, const void *vb)
+{
+
+	uint32_t a = *(uint32_t *)va;

The comment you added is definitely helpful, but I still think that this line is a little magical. *va isn't really a pointer to a uint32_t, but a pointer to the start of a triplet, which just *happens* to have a 4-byte integer at the beginning of it.

I don't think there's a way to improve this much more than we already have, though. Populating a triplet struct to just dereference the first field feels wasteful and slow. So I think what you have here makes sense to me.

Quoted 26 lines
pack-bitmap.c +26 -0
+static uint32_t bsearch_pos(struct bitmap_index *bitmap_git,
+			    struct object_id *oid,
+			    uint32_t *result)
+{
+	int found;
+
+	if (bitmap_is_midx(bitmap_git))
+		found = bsearch_midx(oid, bitmap_git->midx, result);
+	else
+		found = bsearch_pack(oid, bitmap_git->pack, result);
+
+	return found;
+}
+
+/*
+ * `bsearch_triplet` function searches for the raw triplet having
+ * commit position same as `commit_pos` and fills `triplet`
+ * object from the raw triplet. Returns 1 on success and 0
+ * on failure.
+ */
+static int bsearch_triplet(uint32_t *commit_pos,
+			   struct bitmap_index *bitmap_git,
+			   struct bitmap_lookup_table_triplet *triplet)
+{
+	unsigned char *p = bsearch(commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+				   BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH, triplet_cmp);

pack-bitmap.c struct include_data { · line 665

Quoted 10 lines
pack-bitmap.c +10 -0
+
+	if (!p)
+		return 0;
+	triplet->commit_pos = get_be32(p);
+	p += sizeof(uint32_t);
+	triplet->offset = get_be64(p);
+	p += sizeof(uint64_t);
+	triplet->xor_row = get_be32(p);
+	return 1;
+}

This implementation jumped out as being quite similar to lookup_table_get_triplet(). Ultimately they both end up filling a triplet struct based on some position p within the bitmap. The main difference being that in lookup_table_get_triplet(), p comes from a numeric position which indexes into the table, while in bsearch_triplet() the position p is given to us by a call to bsearch().

I wonder if it would be worth extracting the common part of: given a pointer p and a triplet struct, read the triplet beginning at p into the struct.

lookup_table_get_triplet() could compute p and then return the result of calling the new auxiliary function with that p. Similarly for bsearch_triplet(), it would call that auxiliary function with the pointer it got from calling bsearch(), or return 0 if no match was found.

It's a minor point, but I think it would help us clean up the implementation a little bit.

Quoted 22 lines
pack-bitmap.c +22 -0
+
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+					  struct commit *commit)
+{
+	uint32_t commit_pos, xor_row;
+	uint64_t offset;
+	int flags;
+	struct bitmap_lookup_table_triplet triplet;
+	struct object_id *oid = &commit->object.oid;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap = NULL;
+
+	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
+
+	if (!found)
+		return NULL;
+
+	if (!bsearch_triplet(&commit_pos, bitmap_git, &triplet))
+		return NULL;
+
+	offset = triplet.offset;
+	xor_row = triplet.xor_row;

pack-bitmap.c struct include_data { · line 697

Quoted 10 lines
pack-bitmap.c +10 -0
+
+	if (xor_row != 0xffffffff) {
+		int xor_flags;
+		khiter_t hash_pos;
+		uint64_t offset_xor;
+		struct bitmap_lookup_table_xor_item *xor_items;
+		struct bitmap_lookup_table_xor_item xor_item;
+		size_t xor_items_nr = 0, xor_items_alloc = 64;
+
+		ALLOC_ARRAY(xor_items, xor_items_alloc);

This ALLOC_ARRAY() looks great to me. I wonder if we could amortize the cost of allocating in this (somewhat) hot function by treating the xor_items array as a reusable static buffer where we reset xor_items_nr to 0 when entering this function.

pack-bitmap.c struct include_data { · line 703

Quoted 6 lines
pack-bitmap.c +6 -0
+		while (xor_row != 0xffffffff) {
+			struct object_id xor_oid;
+
+			if (xor_items_nr + 1 >= bitmap_git->entry_count) {
+				free(xor_items);
+				error(_("corrupt bitmap lookup table: xor chain exceed entry count"));

I think we can probably die() here, we're pretty much out of luck in this case.

pack-bitmap.c struct include_data { · line 715

Quoted 12 lines
pack-bitmap.c +12 -0
+				return NULL;
+			}
+
+			if (lookup_table_get_triplet(bitmap_git, xor_row, &triplet) < 0)
+				return NULL;
+
+			offset_xor = triplet.offset;
+
+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, triplet.commit_pos) < 0) {
+				free(xor_items);
+				error(_("corrupt bitmap lookup table: commit index %u out of range"),
+					triplet.commit_pos);

Same here.

Quoted 9 lines
pack-bitmap.c +9 -0
+				return NULL;
+			}
+
+			hash_pos = kh_get_oid_map(bitmap_git->bitmaps, xor_oid);
+
+			/*
+			 * If desired bitmap is already stored, we don't need
+			 * to iterate further. Because we know that bitmaps
+			 * that are needed to be parsed to parse this bitmap

pack-bitmap.c struct include_data { · line 734

Quoted 10 lines
pack-bitmap.c +10 -0
+			 * has already been stored. So, assign this stored bitmap
+			 * to the xor_bitmap.
+			 */
+			if (hash_pos < kh_end(bitmap_git->bitmaps) &&
+			    (xor_bitmap = kh_value(bitmap_git->bitmaps, hash_pos)))
+				break;
+
+			ALLOC_GROW(xor_items, xor_items_nr + 1, xor_items_alloc);
+			xor_items[xor_items_nr++] = (struct bitmap_lookup_table_xor_item) {.oid = xor_oid,
+											   .offset = offset_xor};

This style of initialization is somewhat uncommon for Git's codebase. It might be a little more natural to write something like:

    xor_items[xor_items_nr].oid = xor_oid;
    xor_items[xor_items_nr].offset = offset_xor;
    xor_items_nr++;

But the struct-copying for xor_oid is definitely uncommon for us. We should use the oidcpy() helper there instead. Or better yet, pass a pointer to &xor_items[xor_items_nr].oid as the second argument to nth_bitmap_object_oid() to avoid the copy altogether.

pack-bitmap.c struct include_data { · line 743

Quoted 9 lines
pack-bitmap.c +9 -0
+			xor_row = triplet.xor_row;
+		}
+
+		while (xor_items_nr) {
+			xor_item = xor_items[xor_items_nr - 1];
+			offset_xor = xor_item.offset;
+
+			bitmap_git->map_pos = offset_xor;
+			if (bitmap_git->map_size - bitmap_git->map_pos < 6) {

Should we extract 6 out to a named constant?

Thanks,
Taylor

#96 lore raw

Re: [PATCH v3 5/6] bitmap-lookup-table: add performance tests for lookup table

In reply to Abhradeep Chakraborty via GitGitGadget
Quoted 6 lines · Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add performance tests to verify the performance of lookup table with pack.writeReverseIndex enabled. This is to check the performance when the above configuration is set.

Quoted 7 lines

Lookup table makes Git run faster in most of the cases. Below is the result of t/perf/p5310-pack-bitmaps.sh.perf/p5326-multi-pack-bitmaps.sh gives similar result. The repository used in the test is linux kernel.

Test this tree
---------------------------------------------------------------------------
5310.4: repack to disk (lookup=false) 296.55(256.53+14.52)

Having "lookup=false" in this test definitely helps visually differentiate which tests have a bitmap with and without the lookup table.

I think we should take a slightly different approach for these performance tests. I think the first change to the t/perf tests in this series should only enable pack.writeReverseIndex. That patch would be a good place to highlight the benefit of enabling the on-disk reverse index by showing a before and after of running p5310 before and after that commit.

Then the patch after that should look like this one, which runs the suite with and without the lookup table. That should give us a sense of:

- bitmaps without a lookup table or reverse index
- bitmaps without a lookup table, but with a reverse index
- bitamps with a reverse index and a lookup table

...which I think are the most interesting combinations (I wouldn't expect many or any users to have lookup tables enabled without reverse indexes).

I think that would allow us to drop the last patch in this version of the series. But I'm definitely open to other testing strategies for the performance tests (including this one!) if you have different thoughts about what the best way to go about this is.

Thanks,
Taylor

#76 lore raw

Re: [PATCH v3 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Taylor Blau
Quoted 20 lines · Taylor Blau
Quoted 11 lines · Philip Oakley
Quoted 11 lines
Quoted 11 lines

Not sure if this is new in this extension, but should there be a link or two to the basics of XOR compression and some of the bitmap look up techniques?

It's not always obvious if these techniques are 'heuristic' and only have partial commit data, or they have all the commits listed, Nor how/why they work. My point is more about giving new readers a hand-up in their understanding, rather than simple implementation details for those who already know what is going on. For example, are there any external articles that you found helpful in getting started that could be referenced somewhere in the docs?

Quoted 8 lines
Quoted 3 lines

As this series is only about adding a lookup-table extension (and not about bitmap itself), I am not sure whether it's good to include those things in this series.

Thanks for the clarification. I must have slight misread some of the discussions and falsely thought it was the XOR compression (which is a technique I wasn't really aware of), that was being provided by the extension - Where would it be best for me to look up the background to your "extension" project?

Quoted 6 lines

Yeah, Abhradeep is right that the XOR compression isn't new, we already serialize bitmaps with optional XOR offsets. The gist is that we give an offset of some previous bitmap that is used to compress the current one by XORing the bits in the current bitamp with the previous one. These XOR-compressed bitmaps are often sparse, so they compress well and reduce the overall size of the .bitmap.

I was thinking of a short paragraph that covers the broader 'why' aspects, rather than the what/how. For me, XOR is a 'new' compression method that (IIUC) takes advantage of certain features of the way the data is arranged, such that the XOR has lots of leading zeros, leading to the compression mentioned.

I think it's that we sort on oid name, so that despite the oid being long, we have (typically) sufficient oids that the leading XOR bits of adjacent pairs allows effective compression. But I could have guessed wildly wrong.

I'd been looking at
https://www.timescale.com/blog/time-series-compression-algorithms-explained/
which gave an overview for sorted floats.
I'd not had time to review the paper "Gorilla: A Fast, Scalable, In-Memory Time Series Database"
http://www.vldb.org/pvldb/vol8/p1816-teller.pdf

Quoted 5 lines

A slightly more detailed overview can be found in Documentation/technical/bitmap-format.txt under the bullet point reading "1-byte XOR-offset".

A separate point is the linkage (or not) between the older reachability bit maps, and the commit graph, which sound to be independent options and features, yet appear rather interrelated.

Thanks

Philip

#81 lore raw

Re: [PATCH v3 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Taylor Blau
Quoted 6 lines · Taylor Blau

By the end of this loop, we have an array commit_positions which maps the ith selected commit to its lexical position among all objects in the bitmap. IOW, commit_positions[i] = j means the ith selected commit can be found at index j among all objects in the pack/MIDX in their lexical order.

Right.

Quoted 11 lines

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 729

Quoted 6 lines
pack-bitmap-write.c +6 -0
+     /*
+      * At the end of this sort table[j] = i means that the i'th
+      * bitmap corresponds to j'th bitmapped commit in lex order of
+      * OIDs.
+      */
+     QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);

And then we sort table by treating its values as indexes into commit_positions. Here's where I'm not sure that I follow what's going on. You say above that table[j] = i, where i corresponds to the order of selected commits, and j is in lexical order.

Correct.

Quoted 15 lines

If that's the case, then I'd expect that printing index[table[j]] for increasing j would output OIDs in increasing lexical order. But that doesn't quite seem to be the case. From a debugger session that has a breakpoint after computing and sorting table, along with building table_inv:

    (gdb) p oid_to_hex(&index[table[0]]->oid)
    $17 = 0x555555983ea0 <hexbuffer> "0006763074748d43b539c1c8e8882c08034ab178"
    (gdb) p oid_to_hex(&index[table[1]]->oid)
    $18 = 0x555555983ee1 <hexbuffer+65> "001ce83dd43f03dcfc67f29d38922e4a9682aab0"
    (gdb) p oid_to_hex(&index[table[2]]->oid)
    $19 = 0x555555983f22 <hexbuffer+130> "002db882ece2ab6a240e495a169c6e06422289c8"
    (gdb) p oid_to_hex(&index[table[3]]->oid)
    $20 = 0x555555983f63 <hexbuffer+195> "0007a5feb040e1ff704f3ad636619ddca3e7382b"
Quoted 5 lines

that doesn't look like the OIDs are increasing in lexical order.

I'm not quite sure if I'm even looking at the right thing, or if this is to be expected, or if the comment isn't quite accurate. If you could help clarify what's going on here, that would be great.

I think you're not looking at the right thing. you should look at writer.selected[table[i]].commit->object.oid instead. I think the order of index[]
is not the same as the pack index (or midx).

I am saying this because if we use the pos variable (that we get from commit_bitmap_writer_pos(&writer.selected[table[i]].commit->object.oid, index, index_nr)) in fprintf(stderr, "commit hex: %s\n", &index[pos]->oid);, you'll see that &index[pos]->oid and &writer.selected[table[i]].commit->object.oid are not same. So, If you do -

  int spos = commit_bitmap_writer_pos(&index[pos]->oid, index, index_nr);

you'll see spos is not equal to pos.

Quoted 10 lines

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 735

Quoted 5 lines
pack-bitmap-write.c +5 -0
+     /* table_inv helps us discover that relationship (i'th bitmap
+      * to j'th commit by j = table_inv[i])
+      */
+     for (i = 0; i < writer.selected_nr; i++)
+             table_inv[table[i]] = i;

This part makes sense, as does the rest of the implementation.

Thanks,
Taylor

#89 lore raw

Re: [PATCH v3 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau
Quoted 17 lines · Taylor Blau

pack-bitmap.c struct include_data { · line 620

Quoted 10 lines · Abhradeep Chakraborty via GitGitGadget
pack-bitmap.c +10 -0
+/*
+ * Searches for a matching triplet. `va` is a pointer
+ * to the wanted commit position value. `vb` points to
+ * a triplet in lookup table. The first 4 bytes of each
+ * triplet (pointed by `vb`) are compared with `*va`.
+ */
+static int triplet_cmp(const void *va, const void *vb)
+{
+
+     uint32_t a = *(uint32_t *)va;

The comment you added is definitely helpful, but I still think that this line is a little magical. *va isn't really a pointer to a uint32_t, but a pointer to the start of a triplet, which just *happens* to have a 4-byte integer at the beginning of it.

Are you sure about this? As far as I know, the first parameter of such comparing functions is always a pointer to the given key that we need to search for and the second parameter points to each element of an array.

I think "va is a pointer to the wanted commit position value" is not that descriptive. Maybe "va` is a pointer to the given key" is better. What do you think?

Quoted 34 lines
Quoted 11 lines

+ * bsearch_triplet function searches for the raw triplet having
+ * commit position same as commit_pos and fills triplet
+ * object from the raw triplet. Returns 1 on success and 0
+ * on failure.
+ */

pack-bitmap.c +6 -0
+static int bsearch_triplet(uint32_t *commit_pos,
+                        struct bitmap_index *bitmap_git,
+                        struct bitmap_lookup_table_triplet *triplet)
+{
+     unsigned char *p = bsearch(commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+                                BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH, triplet_cmp);

pack-bitmap.c struct include_data { · line 665

Quoted 10 lines
pack-bitmap.c +10 -0
+
+     if (!p)
+             return 0;
+     triplet->commit_pos = get_be32(p);
+     p += sizeof(uint32_t);
+     triplet->offset = get_be64(p);
+     p += sizeof(uint64_t);
+     triplet->xor_row = get_be32(p);
+     return 1;
+}

This implementation jumped out as being quite similar to lookup_table_get_triplet(). Ultimately they both end up filling a triplet struct based on some position p within the bitmap. The main difference being that in lookup_table_get_triplet(), p comes from a numeric position which indexes into the table, while in bsearch_triplet() the position p is given to us by a call to bsearch().

I wonder if it would be worth extracting the common part of: given a pointer p and a triplet struct, read the triplet beginning at p into the struct.

Quoted 8 lines

lookup_table_get_triplet() could compute p and then return the result of calling the new auxiliary function with that p. Similarly for bsearch_triplet(), it would call that auxiliary function with the pointer it got from calling bsearch(), or return 0 if no match was found.

It's a minor point, but I think it would help us clean up the implementation a little bit.

Sure! That would be a great idea!

Quoted 14 lines
Quoted 1 line

+ ALLOC_ARRAY(xor_items, xor_items_alloc);

This ALLOC_ARRAY() looks great to me. I wonder if we could amortize the cost of allocating in this (somewhat) hot function by treating the xor_items array as a reusable static buffer where we reset xor_items_nr to 0 when entering this function.

pack-bitmap.c struct include_data { · line 703

Quoted 6 lines
pack-bitmap.c +6 -0
+             while (xor_row != 0xffffffff) {
+                     struct object_id xor_oid;
+
+                     if (xor_items_nr + 1 >= bitmap_git->entry_count) {
+                             free(xor_items);
+                             error(_("corrupt bitmap lookup table: xor chain exceed entry count"));
Quoted 7 lines

I think we can probably die() here, we're pretty much out of luck in this case.
...

pack-bitmap.c struct include_data { · line 715

Quoted 2 lines
pack-bitmap.c +2 -0
+                             error(_("corrupt bitmap lookup table: commit index %u out of range"),
+                                     triplet.commit_pos);

Same here.

I didn't use die() here because I thought returning NULL would be a better idea. In that case, Git can still do its job by using the traditional approach - traversing between objects. load_bitmap_entries_v1 also returns NULL if an error occurs. What do you think?

Quoted 7 lines

pack-bitmap.c struct include_data { · line 734

Quoted 3 lines
pack-bitmap.c +3 -0
+                     ALLOC_GROW(xor_items, xor_items_nr + 1, xor_items_alloc);
+                     xor_items[xor_items_nr++] = (struct bitmap_lookup_table_xor_item) {.oid = xor_oid,
+                                                                                        .offset = offset_xor};

This style of initialization is somewhat uncommon for Git's codebase. It might be a little more natural to write something like:

Quoted 8 lines
    xor_items[xor_items_nr].oid = xor_oid;
    xor_items[xor_items_nr].offset = offset_xor;
    xor_items_nr++;

But the struct-copying for xor_oid is definitely uncommon for us. We should use the oidcpy() helper there instead. Or better yet, pass a pointer to &xor_items[xor_items_nr].oid as the second argument to nth_bitmap_object_oid() to avoid the copy altogether.

Ok, got it.

Quoted 4 lines

Review

Quoted 2 lines
(quoted diff) +2 -0
+                     bitmap_git->map_pos = offset_xor;
+                     if (bitmap_git->map_size - bitmap_git->map_pos < 6) {

Should we extract 6 out to a named constant?

Ok, sure!

Thanks :)

Quoted 3 lines

Thanks,
Taylor

#97 lore raw

Re: [PATCH v3 5/6] bitmap-lookup-table: add performance tests for lookup table

In reply to Taylor Blau
Quoted 19 lines · Taylor Blau

Having "lookup=false" in this test definitely helps visually differentiate which tests have a bitmap with and without the lookup table.

I think we should take a slightly different approach for these performance tests. I think the first change to the t/perf tests in this series should only enable pack.writeReverseIndex. That patch would be a good place to highlight the benefit of enabling the on-disk reverse index by showing a before and after of running p5310 before and after that commit.

Then the patch after that should look like this one, which runs the suite with and without the lookup table. That should give us a sense of:

- bitmaps without a lookup table or reverse index
- bitmaps without a lookup table, but with a reverse index
- bitamps with a reverse index and a lookup table

Quoted 8 lines

...which I think are the most interesting combinations (I wouldn't expect many or any users to have lookup tables enabled without reverse indexes).

I think that would allow us to drop the last patch in this version of the series. But I'm definitely open to other testing strategies for the performance tests (including this one!) if you have different thoughts about what the best way to go about this is.

Got it. Thanks !

Quoted 2 lines

Thanks,
Taylor

#77 lore raw

Re: [PATCH v3 1/6] Documentation/technical: describe bitmap lookup table extension

In reply to Philip Oakley
Quoted 21 lines · Philip Oakley
Quoted 15 lines · Abhradeep Chakraborty

Hello Philip,

Quoted 11 lines · Philip Oakley

Not sure if this is new in this extension, but should there be a link or two to the basics of XOR compression and some of the bitmap look up techniques?

It's not always obvious if these techniques are 'heuristic' and only have partial commit data, or they have all the commits listed, Nor how/why they work. My point is more about giving new readers a hand-up in their understanding, rather than simple implementation details for those who already know what is going on. For example, are there any external articles that you found helpful in getting started that could be referenced somewhere in the docs?

Quoted 3 lines

As this series is only about adding a lookup-table extension (and not about bitmap itself), I am not sure whether it's good to include those things in this series.

Quoted 5 lines

Thanks for the clarification. I must have slight misread some of the discussions and falsely thought it was the XOR compression (which is a technique I wasn't really aware of), that was being provided by the extension - Where would it be best for me to look up the background to your "extension" project?

Sorry that I missed this message. I got the information related to this project from the gsoc project ideas[1] page, additionally you can see the comments[2].

[1] https://git.github.io/SoC-2022-Ideas/
[2] https://lore.kernel.org/git/[email protected]/

Quoted 7 lines
Quoted 6 lines

(As the name
suggests, this file should only contain format details of bitmaps). That file would provide the answers of "Why bitmaps", "how they are stored", "How they are fetched", "how they work with pack-objects, git-fetch, midx etc.", "Detailed explanation of each bitmap extension" , and lastly "what are the future works" (if any).

Quoted 5 lines

One thing I've realised on reflection is that I'm unclear how the 'reachability bitmaps' and the 'commit-graph file' techniques relate to each other (and to the ODB DAG), and what features they pick out within their heuristic, explained at just enough level to allow folks to appreciate what the options that select them will do for their use case.

I am not familiar with 'commit-graph file', so I can't tell you about that. But for bitmaps, you can look at the introductory patches[3]. After that, if you wish, you can also inspect the code related to bitmaps.

[3] https://github.com/gitster/git/commit/e127310

Quoted 8 lines
Quoted 1 line

What do you think?

I'd be happy to collate contributions, suggestions and thoughts.

Trying to create these good introductory descriptions can be really difficult, as you can only step into the same river once (the 'reading for the first time problem' of not being able to un-hear the explanations of others when reading a 2nd draft...)

I agree ;-)

Thanks :)

#82 lore raw

Re: [PATCH v3 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty
Quoted 33 lines · Abhradeep Chakraborty
Quoted 15 lines

If that's the case, then I'd expect that printing index[table[j]] for increasing j would output OIDs in increasing lexical order. But that doesn't quite seem to be the case. From a debugger session that has a breakpoint after computing and sorting table, along with building table_inv:

    (gdb) p oid_to_hex(&index[table[0]]->oid)
    $17 = 0x555555983ea0 <hexbuffer> "0006763074748d43b539c1c8e8882c08034ab178"
    (gdb) p oid_to_hex(&index[table[1]]->oid)
    $18 = 0x555555983ee1 <hexbuffer+65> "001ce83dd43f03dcfc67f29d38922e4a9682aab0"
    (gdb) p oid_to_hex(&index[table[2]]->oid)
    $19 = 0x555555983f22 <hexbuffer+130> "002db882ece2ab6a240e495a169c6e06422289c8"
    (gdb) p oid_to_hex(&index[table[3]]->oid)
    $20 = 0x555555983f63 <hexbuffer+195> "0007a5feb040e1ff704f3ad636619ddca3e7382b"
Quoted 5 lines

that doesn't look like the OIDs are increasing in lexical order.

I'm not quite sure if I'm even looking at the right thing, or if this is to be expected, or if the comment isn't quite accurate. If you could help clarify what's going on here, that would be great.

I think you're not looking at the right thing. you should look at writer.selected[table[i]].commit->object.oid instead. I think the order of index[]
is not the same as the pack index (or midx).

I am saying this because if we use the pos variable (that we get from commit_bitmap_writer_pos(&writer.selected[table[i]].commit->object.oid, index, index_nr)) in fprintf(stderr, "commit hex: %s\n", &index[pos]->oid);, you'll see that &index[pos]->oid and &writer.selected[table[i]].commit->object.oid are not same. So, If you do -

Quoted 3 lines
  int spos = commit_bitmap_writer_pos(&index[pos]->oid, index, index_nr);

you'll see spos is not equal to pos.

index there comes from the list of objects that pack-objects or the MIDX told us about, and it's sorted in lexical order (via write_pack_file() -> stage_tmp_packfiles() -> write_idx_file()).

So I think this implementation is indexing the commits by the order they appearn in the writer.selected array, *not* by the order they appear in the index.

For what it's worth, I think the latter ordering makes more sense to use to refer to individual objects. But we should be consistent with our choice here and what's in the documentation. And right now I think we're not, since the documentation change in the first patch says we write the commit_pos field in order of the index:

* {empty}
commit_pos (4 byte integer, network byte order): ::
It stores the object position of a commit (in the midx or pack index).

Thanks,
Taylor

#90 lore raw

Re: [PATCH v3 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Abhradeep Chakraborty
Quoted 19 lines · Abhradeep Chakraborty
Quoted 17 lines · Taylor Blau

pack-bitmap.c struct include_data { · line 620

Quoted 10 lines · Abhradeep Chakraborty via GitGitGadget
pack-bitmap.c +10 -0
+/*
+ * Searches for a matching triplet. `va` is a pointer
+ * to the wanted commit position value. `vb` points to
+ * a triplet in lookup table. The first 4 bytes of each
+ * triplet (pointed by `vb`) are compared with `*va`.
+ */
+static int triplet_cmp(const void *va, const void *vb)
+{
+
+     uint32_t a = *(uint32_t *)va;

The comment you added is definitely helpful, but I still think that this line is a little magical. *va isn't really a pointer to a uint32_t, but a pointer to the start of a triplet, which just *happens* to have a 4-byte integer at the beginning of it.

Quoted 8 lines

Are you sure about this? As far as I know, the first parameter of such comparing functions is always a pointer to the given key that we need to search for and the second parameter points to each element of an array.

I think "va is a pointer to the wanted commit position value" is not that descriptive. Maybe "va` is a pointer to the given key" is better. What do you think?

Yes, the first argument to the comparison function used in bsearch() is a pointer to some element in the array. I just meant that that array is the bitmap_git->table_lookup region, so each element isn't actually a uint32_t array, but the whole thing is an array of (uint32_t, uint64_t, uint32_t) triplets.

What you wrote here is fine, and I don't even think that the comment needs updating. If you did want to clarify, I think you could say something along the lines of what you wrote above ("va is a pointer to an array element") and add something along the lines of "where the array is the lookup table region of the .bitmap".

Quoted 20 lines
Quoted 20 lines
Quoted 1 line

+ ALLOC_ARRAY(xor_items, xor_items_alloc);

This ALLOC_ARRAY() looks great to me. I wonder if we could amortize the cost of allocating in this (somewhat) hot function by treating the xor_items array as a reusable static buffer where we reset xor_items_nr to 0 when entering this function.

pack-bitmap.c struct include_data { · line 703

Quoted 6 lines
pack-bitmap.c +6 -0
+             while (xor_row != 0xffffffff) {
+                     struct object_id xor_oid;
+
+                     if (xor_items_nr + 1 >= bitmap_git->entry_count) {
+                             free(xor_items);
+                             error(_("corrupt bitmap lookup table: xor chain exceed entry count"));

I think we can probably die() here, we're pretty much out of luck in this case.
...

Quoted 2 lines
pack-bitmap.c +2 -0
+                             error(_("corrupt bitmap lookup table: commit index %u out of range"),
+                                     triplet.commit_pos);
Quoted 7 lines
Quoted 1 line

Same here.

I didn't use die() here because I thought returning NULL would be a better idea. In that case, Git can still do its job by using the traditional approach - traversing between objects. load_bitmap_entries_v1 also returns NULL if an error occurs. What do you think?

Ah, I wasn't aware that our callers are graceful enough to handle this like that. Yes, if we can fallback gracefully, we should, so I think just error()-ing here (and above) is the right choice. Thanks for saying so.

Thanks,
Taylor

#83 lore raw

Re: [PATCH v3 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Taylor Blau
Quoted 35 lines · Taylor Blau
Quoted 33 lines · Abhradeep Chakraborty
Quoted 15 lines

If that's the case, then I'd expect that printing index[table[j]] for increasing j would output OIDs in increasing lexical order. But that doesn't quite seem to be the case. From a debugger session that has a breakpoint after computing and sorting table, along with building table_inv:

    (gdb) p oid_to_hex(&index[table[0]]->oid)
    $17 = 0x555555983ea0 <hexbuffer> "0006763074748d43b539c1c8e8882c08034ab178"
    (gdb) p oid_to_hex(&index[table[1]]->oid)
    $18 = 0x555555983ee1 <hexbuffer+65> "001ce83dd43f03dcfc67f29d38922e4a9682aab0"
    (gdb) p oid_to_hex(&index[table[2]]->oid)
    $19 = 0x555555983f22 <hexbuffer+130> "002db882ece2ab6a240e495a169c6e06422289c8"
    (gdb) p oid_to_hex(&index[table[3]]->oid)
    $20 = 0x555555983f63 <hexbuffer+195> "0007a5feb040e1ff704f3ad636619ddca3e7382b"
Quoted 5 lines

that doesn't look like the OIDs are increasing in lexical order.

I'm not quite sure if I'm even looking at the right thing, or if this is to be expected, or if the comment isn't quite accurate. If you could help clarify what's going on here, that would be great.

I think you're not looking at the right thing. you should look at writer.selected[table[i]].commit->object.oid instead. I think the order of index[]
is not the same as the pack index (or midx).

I am saying this because if we use the pos variable (that we get from commit_bitmap_writer_pos(&writer.selected[table[i]].commit->object.oid, index, index_nr)) in fprintf(stderr, "commit hex: %s\n", &index[pos]->oid);, you'll see that &index[pos]->oid and &writer.selected[table[i]].commit->object.oid are not same. So, If you do -

Quoted 7 lines
Quoted 3 lines
  int spos = commit_bitmap_writer_pos(&index[pos]->oid, index, index_nr);

you'll see spos is not equal to pos.

index there comes from the list of objects that pack-objects or the MIDX told us about, and it's sorted in lexical order (via write_pack_file() -> stage_tmp_packfiles() -> write_idx_file()).

This was a bit strange for me because all the tests were passing. But now I find the reason why your results were not in lexical order. you were doing oid_to_hex(&index[table[i]]->oid) which is not what you intended to do. Let me explain it with a simple workflow -

Suppose 12 commits are selected for bitmaps and are sorted by their date. I will now use their index numbers to denote those commits (i.e. 0 denotes the most recent commit, 1 denotes the second commit in this order and so on..).
So, before that quick sort, table = {0,1, 2, 3, 4, ...,11}. Now suppose, 11th commit is lexically smallest among all the selected commits, 5th commit is the second smallest commit and so on. So, after that quick sort, table array now contains the following - {11, 5, 9, 4,0, 3, ...}.

So, when you do &index[table[0]]->oid, it becomes &index[11]->oid. Similarly, &index[table[1]]->oid becomes &index[5]->oid and so on. That's why you're not getting the oids in lexical order - &index[11]->oid gives the 11th oid in the pack-index and &index[5]->oid gives the 5th oid in the pack-index.

So, the right thing would be to do
&index[commit_positions[table[0]]]->oid,
&index[commit_positions[table[1]]]->oid ...

Here &index[commit_positions[table[0]]]->oid becomes &index[commit_positions[11]]->oid => &index[pos_of_11_commit_with_respect_to_pack_index]->oid which ultimately prints the oid of 11th commit ( among the selected bitmap commits IN THE SELECTED BITMAP COMMIT ORDER) .

I think the comment I added is not that good. The following might be better -

At the end of this sort table[j] = i means that the i'th bitmap corresponds to j'th bitmapped commit (among the selected commits) in lex order of OIDs.

Quoted 2 lines

Thanks,
Taylor

#85 lore raw

Re: [PATCH v3 2/6] pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 689

Quoted 6 lines · Abhradeep Chakraborty via GitGitGadget
pack-bitmap-write.c +5 -0
+static int table_cmp(const void *_va, const void *_vb, void *_data)
+{
+       uint32_t *commit_positions = _data;
+       uint32_t a = commit_positions[*(uint32_t *)_va];
+       uint32_t b = commit_positions[*(uint32_t *)_vb];

This casting and dereferencing are ok because ...

pack-bitmap-write.c static void write_selected_commits_v1(struct hashfile *f, · line 709

Quoted 11 lines
pack-bitmap-write.c +11 -0
+static void write_lookup_table(struct hashfile *f,
+                              struct pack_idx_entry **index,
+                              uint32_t index_nr,
+                              off_t *offsets)
+{
+       uint32_t i;
+       uint32_t *table, *table_inv, *commit_positions;
+
+       ALLOC_ARRAY(table, writer.selected_nr);
+       ALLOC_ARRAY(table_inv, writer.selected_nr);
+       ALLOC_ARRAY(commit_positions, writer.selected_nr);

... table is where _va and _vb will be pointing into.

Quoted 1 line

+ QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);

I started looking at this casting because of something similar in "pack-bitmap: prepare to read lookup table extension". I'm pointing out this instance just to say that it looks ok to me.

Martin

#91 lore raw

Re: [PATCH v3 4/6] pack-bitmap: prepare to read lookup table extension

In reply to Taylor Blau

Hi Abhradeep and Taylor,

I very much enjoy following from a distance Abhradeep's work on this series and all the reviewing and mentoring. I don't grasp anywhere near all the details, but I've looked into this a bit:

Quoted 5 lines · Taylor Blau
Quoted 3 lines · Abhradeep Chakraborty
Quoted 2 lines · Taylor Blau

On Mon, Jul 04, 2022 at 08:46:14AM +0000, Abhradeep Chakraborty via GitGitGadget wrote:

Quoted 15 lines
Quoted 7 lines
Quoted 7 lines
Quoted 7 lines
pack-bitmap.c +7 -0
+/*
+ * Searches for a matching triplet. `va` is a pointer
+ * to the wanted commit position value. `vb` points to
+ * a triplet in lookup table. The first 4 bytes of each
+ * triplet (pointed by `vb`) are compared with `*va`.
+ */
+static int triplet_cmp(const void *va, const void *vb)
Quoted 8 lines
Quoted 8 lines

pack-bitmap.c struct include_data { · line 620

Quoted 3 lines
pack-bitmap.c +3 -0
+{
+
+     uint32_t a = *(uint32_t *)va;

The comment you added is definitely helpful, but I still think that this line is a little magical. *va isn't really a pointer to a uint32_t, but a pointer to the start of a triplet, which just *happens* to have a 4-byte integer at the beginning of it.

Yeah, this all looks quite magical with the casting, and with the asymmetric handling of va and vb.

Quoted 4 lines
Quoted 4 lines

Are you sure about this? As far as I know, the first parameter of such comparing functions is always a pointer to the given key that we need to search for and the second parameter points to each element of an array.

Yes, that matches my understanding and the man-page for bsearch(3):

"The compar routine is expected to have two arguments which point to the key object and to an array member, in that order, [...]"

I think it would help to make this something like

static int triplet_cmp(const void *key, const void *array_item)

to really highlight this asymmetric nature of this function, or to make clear how the values flow through our call-chain through something like

static int triplet_cmp(const void *commit_pos, const void *table_entry)

Because we really do rely on this promise of bsearch(3) -- if we would instantiate a 'dummy' triplet carrying the key, we wouldn't need to (but we would instead need to have our cmp function constantly re-read the same value, including doing the byteswap).

Would it make sense to let the const void *key directly carry the 32-bit value and hope that sizeof(key) >= sizeof(uint32_t)? That's probably too magical, "just" to save on dereferencing.

One thing that could perhaps make things clearer is if bsearch_triplet() did take the position directly, rather than as a pointer:

-static int bsearch_triplet(uint32_t *commit_pos,
+static int bsearch_triplet(uint32_t commit_pos,
struct bitmap_index *bitmap_git, struct bitmap_lookup_table_triplet *triplet)
{
- unsigned char *p = bsearch(commit_pos,
bitmap_git->table_lookup, bitmap_git->entry_count,
+ unsigned char *p = bsearch(&commit_pos,
bitmap_git->table_lookup, bitmap_git->entry_count,
BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH,
triplet_cmp);

Also, maybe s/bsearch_triplet/&_by_pos/ could clarify the intent of this function?

Quoted 5 lines
Quoted 3 lines

I think "va is a pointer to the wanted commit position value" is not that descriptive. Maybe "va` is a pointer to the given key" is better. What do you think?

Yes, the first argument to the comparison function used in bsearch() is

s/first/second/

Quoted 10 lines

a pointer to some element in the array. I just meant that that array is the bitmap_git->table_lookup region, so each element isn't actually a uint32_t array, but the whole thing is an array of (uint32_t, uint64_t, uint32_t) triplets.

What you wrote here is fine, and I don't even think that the comment needs updating. If you did want to clarify, I think you could say something along the lines of what you wrote above ("va is a pointer to an array element") and add something along the lines of "where the array is the lookup table region of the .bitmap".

I mentioned a few ideas for clarifying things above. I do think it would be a good idea to differentiate the names of va and vb to make the fundamental asymmetry between them clearer. The rest of my comments are really just musings.

I originally started looking at this because I wanted to see why the casting to a uint32_t * and dereferencing it was safe. The reason is, we're always handling the same pointer to a uint32_t on the stack, so alignment is guaranteed.

Martin

#92 lore raw

Re: [PATCH v3 4/6] pack-bitmap: prepare to read lookup table extension

Quoted 6 lines · Martin Ågren

Hi Abhradeep and Taylor,

I very much enjoy following from a distance Abhradeep's work on this series and all the reviewing and mentoring. I don't grasp anywhere near all the details, but I've looked into this a bit:

Thanks!

Quoted 11 lines

"The compar routine is expected to have two arguments which point to the key object and to an array member, in that order, [...]"

I think it would help to make this something like

static int triplet_cmp(const void *key, const void *array_item)

to really highlight this asymmetric nature of this function, or to make clear how the values flow through our call-chain through something like

static int triplet_cmp(const void *commit_pos, const void *table_entry)

Nice. Will update it.

Quoted 3 lines

Would it make sense to let the const void *key directly carry the 32-bit value and hope that sizeof(key) >= sizeof(uint32_t)? That's probably too magical, "just" to save on dereferencing.

I do not have any particular opinion here. I will do whatever you think is best.

Quoted 17 lines

One thing that could perhaps make things clearer is if bsearch_triplet() did take the position directly, rather than as a pointer:

(quoted diff) +1 -2
-static int bsearch_triplet(uint32_t *commit_pos,
+static int bsearch_triplet(uint32_t commit_pos,
                           struct bitmap_index *bitmap_git,
                           struct bitmap_lookup_table_triplet *triplet)
 {
-       unsigned char *p = bsearch(commit_pos,

bitmap_git->table_lookup, bitmap_git->entry_count,
+ unsigned char *p = bsearch(&commit_pos,
bitmap_git->table_lookup, bitmap_git->entry_count,
BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH,
triplet_cmp);

Quoted 2 lines

Also, maybe s/bsearch_triplet/&_by_pos/ could clarify the intent of this function?

Ok, sure!

Thanks :)

#93 lore raw

Re: [PATCH v3 4/6] pack-bitmap: prepare to read lookup table extension

Quoted 8 lines · Abhradeep Chakraborty
Quoted 4 lines · Martin Ågren

Would it make sense to let the const void *key directly carry the 32-bit value and hope that sizeof(key) >= sizeof(uint32_t)? That's probably too magical, "just" to save on dereferencing.

I do not have any particular opinion here. I will do whatever you think is best.

To be honest, I think it would be better not to do that. I floated it as a random idea, but it's somewhere in the vicinity of undefined behavior, and in any case, it might be a bit too tricky. If we're doing a byteswap anyway (on virtually all platforms) and doing a bunch of comparisons, trying to save on a dereference doesn't seem worth the increased "huh" factor.

Martin

#105 lore raw

PATCH v4 1/6 Documentation/technical: describe bitmap lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, Git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx or pack, along with their offset and xor offset. This way git can load only the necessary bitmaps without loading the previous bitmaps.

Older versions of Git ignore the lookup table extension and don't throw any kind of warning or error while parsing the bitmap file.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

---
 Documentation/technical/bitmap-format.txt | 39 +++++++++++++++++++++++
 1 file changed, 39 insertions(+)
Documentation/technical/bitmap-format.txt +39 -0
index 04b3ec21785..c30dc177643 100644
@@ -67,6 +67,17 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` <commit_pos, offset, xor_row>
+			triplets. The format and meaning of the table is described
+			below.
++
+NOTE: Unlike the xor_offset used to compress an individual bitmap,
+`xor_row` stores an *absolute* index into the lookup table, not a location
+relative to the current entry.
+
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +216,31 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last `N * (4 + 8 + 4)`
+bytes (preceding the name-hash cache and trailing hash) of the `.bitmap`
+file contains a lookup table specifying the information needed to get
+the desired bitmap from the entries without parsing previous unnecessary
+bitmaps.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the table
+contains a list of `nr_entries` <commit_pos, offset, xor_row> triplets
+(sorted in the ascending order of `commit_pos`). The content of i'th
+triplet is -
+
+	* {empty}
+	commit_pos (4 byte integer, network byte order): ::
+	It stores the object position of a commit (in the midx or pack
+	index).
+
+	* {empty}
+	offset (8 byte integer, network byte order): ::
+	The offset from which that commit's bitmap can be read.
+
+	* {empty}
+	xor_row (4 byte integer, network byte order): ::
+	The position of the triplet whose bitmap is used to compress
+	this one, or `0xffffffff` if no such bitmap exists.
Signature · 1 line
gitgitgadget
#106 lore raw

PATCH v4 2/6 pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documented by an earlier change, but Git does not yet know how to write that extension.

Teach Git to write bitmap lookup table extension. The table contains the list of N <commit_pos, offset, xor_row>` triplets. These triplets are sorted according to their commit pos (ascending order). The meaning of each data in the i'th triplet is given below:

- commit_pos stores commit position (in the pack-index or midx). It is a 4 byte network byte order unsigned integer.

- offset is the position (in the bitmap file) from which that commit's bitmap can be read.

- xor_row is the position of the triplet in the lookup table whose bitmap is used to compress this bitmap, or 0xffffffff if no such bitmap exists.

---
 pack-bitmap-write.c | 112 ++++++++++++++++++++++++++++++++++++++++----
 pack-bitmap.h       |   5 +-
 2 files changed, 107 insertions(+), 10 deletions(-)
pack-bitmap-write.c +104 -8
index c43375bd344..9843790cb60 100644
@@ -650,20 +650,19 @@ static const struct object_id *oid_access(size_t pos, const void *table)
 
 static void write_selected_commits_v1(struct hashfile *f,
 				      struct pack_idx_entry **index,
-				      uint32_t index_nr)
+				      uint32_t index_nr,
+				      off_t *offsets,
+				      uint32_t *commit_positions)
 {
 	int i;
 
 	for (i = 0; i < writer.selected_nr; ++i) {
 		struct bitmapped_commit *stored = &writer.selected[i];
 
-		int commit_pos =
-			oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
+		if (offsets)
+			offsets[i] = hashfile_total(f);
 
-		if (commit_pos < 0)
-			BUG("trying to write commit not in index");
-
-		hashwrite_be32(f, commit_pos);
+		hashwrite_be32(f, commit_positions[i]);
 		hashwrite_u8(f, stored->xor_offset);
 		hashwrite_u8(f, stored->flags);
 
@@ -671,6 +670,81 @@ static void write_selected_commits_v1(struct hashfile *f,
 	}
 }
 
+static int table_cmp(const void *_va, const void *_vb, void *_data)
+{
+	uint32_t *commit_positions = _data;
+	uint32_t a = commit_positions[*(uint32_t *)_va];
+	uint32_t b = commit_positions[*(uint32_t *)_vb];
+
+	if (a > b)
+		return 1;
+	else if (a < b)
+		return -1;
+
+	return 0;
+}
+
+static void write_lookup_table(struct hashfile *f,
+			       struct pack_idx_entry **index,
+			       uint32_t index_nr,
+			       off_t *offsets,
+			       uint32_t *commit_positions)
+{
+	uint32_t i;
+	uint32_t *table, *table_inv;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+
+	/*
+	 * At the end of this sort table[j] = i means that the i'th
+	 * bitmap corresponds to j'th bitmapped commit (among the selected
+	 * commits) in lex order of OIDs.
+	 */
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);
+
+	/* table_inv helps us discover that relationship (i'th bitmap
+	 * to j'th commit by j = table_inv[i])
+	 */
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;
+
+	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		uint32_t xor_offset = selected->xor_offset;
+		uint32_t xor_row;
+
+		if (xor_offset) {
+			/*
+			 * xor_index stores the index (in the bitmap entries)
+			 * of the corresponding xor bitmap. But we need to convert
+			 * this index into lookup table's index. So, table_inv[xor_index]
+			 * gives us the index position w.r.t. the lookup table.
+			 *
+			 * If "k = table[i] - xor_offset" then the xor base is the k'th
+			 * bitmap. `table_inv[k]` gives us the position of that bitmap
+			 * in the lookup table.
+			 */
+			uint32_t xor_index = table[i] - xor_offset;
+			xor_row = table_inv[xor_index];
+		} else {
+			xor_row = 0xffffffff;
+		}
+
+		hashwrite_be32(f, commit_positions[table[i]]);
+		hashwrite_be64(f, (uint64_t)offsets[table[i]]);
+		hashwrite_be32(f, xor_row);
+	}
+	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
+
+	free(table);
+	free(table_inv);
+}
+
 static void write_hash_cache(struct hashfile *f,
 			     struct pack_idx_entry **index,
 			     uint32_t index_nr)
@@ -695,8 +769,10 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 {
 	static uint16_t default_version = 1;
 	static uint16_t flags = BITMAP_OPT_FULL_DAG;
+	off_t *offsets = NULL;
 	struct strbuf tmp_file = STRBUF_INIT;
 	struct hashfile *f;
+	uint32_t *commit_positions = NULL;
 
 	struct bitmap_disk_header header;
 
@@ -715,7 +791,25 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 	dump_bitmap(f, writer.trees);
 	dump_bitmap(f, writer.blobs);
 	dump_bitmap(f, writer.tags);
-	write_selected_commits_v1(f, index, index_nr);
+
+	ALLOC_ARRAY(commit_positions, writer.selected_nr);
+	for (uint32_t i = 0; i < writer.selected_nr; ++i) {
+		struct bitmapped_commit *stored = &writer.selected[i];
+		int commit_pos = oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
+
+		if (commit_pos < 0)
+			BUG(_("trying to write commit not in index"));
+
+		commit_positions[i] = commit_pos;
+	}
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		CALLOC_ARRAY(offsets, index_nr);
+
+	write_selected_commits_v1(f, index, index_nr, offsets, commit_positions);
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		write_lookup_table(f, index, index_nr, offsets, commit_positions);
 
 	if (options & BITMAP_OPT_HASH_CACHE)
 		write_hash_cache(f, index, index_nr);
@@ -730,4 +824,6 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 		die_errno("unable to rename temporary bitmap file to '%s'", filename);
 
 	strbuf_release(&tmp_file);
+	free(offsets);
+	free(commit_positions);
 }
pack-bitmap.h +3 -2
index 3d3ddd77345..67a9d0fc303 100644
@@ -24,8 +24,9 @@ struct bitmap_disk_header {
 #define NEEDS_BITMAP (1u<<22)
 
 enum pack_bitmap_opts {
-	BITMAP_OPT_FULL_DAG = 1,
-	BITMAP_OPT_HASH_CACHE = 4,
+	BITMAP_OPT_FULL_DAG = 0x1,
+	BITMAP_OPT_HASH_CACHE = 0x4,
+	BITMAP_OPT_LOOKUP_TABLE = 0x10,
 };
 
 enum pack_bitmap_flags {
Signature · 1 line
gitgitgadget
#104 lore raw

v4 cover bitmap: integrate a lookup table extension to the bitmap format

In reply to Abhradeep Chakraborty via GitGitGadget

When parsing the .bitmap file, git loads all the bitmaps one by one even if some of the bitmaps are not necessary. We can remove this overhead by loading only the necessary bitmaps. A look up table extension can solve this issue.

Changes since v3:

* The common code from both lookup_table_get_triplet() and bsearch_triplet_by_pos are moved to lookup_table_get_triplet_by_pointer function
* parameter names of triplet_cmp function is changes (as suggested by Martin)
* xor_items array is now work as reusable static buffer.
* I moved the filling commit_positions array part (from pack-bitmap-write.c) to bitmap_writer_finish function. Because we had to iterate two times for commit positions - one in write_selected_commits_v1 and another in write_lookup_table function. Hope this is acceptable :)
* changes in performance tests (as suggested by Taylor)

Changes since v2:

* Log messages related issues are fixed.
* pack.writeBitmapLookupTable is now by default disabled.
* Documentations are improved.
* xor_row is used instead of xor_pos in triplets.
* In pack-bitmap-write.c, off_t * is used for offsets array (Instead of uint64_t *).
* struct bitmap_lookup_table_triplet is introduced and functions Like triplet_get_offset() and triplet_get_xor_pos() are removed.
* table_size is getting subtracted from index_end irrespective of the value of GIT_TEST_READ_COMMIT_TABLE.
* xor stack filling loop will stop iterating if a xor bitmap is already stored/parsed.
* The stack will now store bitmap_lookup_table_xor_item items Of plain xor_row.
* bitmap related test files are reformatted to allow repeating of tests with bitmap extension enabled.
* comments are added.

Changes since v1:

This is the second version which addressed all (I think) the reviews. Please notify me if some reviews are not addressed :)

* The table size is decreased and the format has also changed. It now contains nr_entries triplets of size 4+8+4 bytes. Each triplet contains the following things - (1) 4 byte commit position (in the pack-index or midx) (2) 8 byte offset and (3) 4 byte xor triplet (i.e. with whose bitmap the current triplet's bitmap has to xor) position.
* Performance tests are splitted into two commits. First contains the actual performance tests and second enables the pack.writeReverseIndex (as suggested by Taylor).
* st_*() functions are used.
* commit order is changed according to Derrick's suggestion.
* Iterative approach is used instead of recursive approach to parse xor bitmaps. (As suggested by Derrick).
* Some minor bug fixes of previous version.

Initial version:

The proposed table has:

* a list of nr_entries object ids. These objects are commits that has bitmaps. Ids are stored in lexicographic order (for better searching).
* a list of <offset, xor-offset> pairs (4-byte integers, network-byte order). The i'th pair denotes the offset and xor-offset(respectively) of the bitmap of i'th commit in the previous list. These two informations are necessary because only in this way bitmaps can be found without parsing all the bitmap.
* a 4-byte integer for table specific flags (none exists currently).

Whenever git want to parse the bitmap for a specific commit, it will first refer to the table and will look for the offset and xor-offset for that commit. Git will then try to parse the bitmap located at the offset position. The xor-offset can be used to find the xor-bitmap for the bitmap(if any).

Abhradeep Chakraborty (6):
Documentation/technical: describe bitmap lookup table extension pack-bitmap-write.c: write lookup table extension
pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
pack-bitmap: prepare to read lookup table extension p5310-pack-bitmaps.sh: enable pack.writeReverseIndex
bitmap-lookup-table: add performance tests for lookup table

 Documentation/config/pack.txt             |   7 +
 Documentation/technical/bitmap-format.txt |  39 ++
 builtin/multi-pack-index.c                |   7 +
 builtin/pack-objects.c                    |   8 +
 midx.c                                    |   3 +
 midx.h                                    |   1 +
 pack-bitmap-write.c                       | 112 ++-
 pack-bitmap.c                             | 278 +++++++-
 pack-bitmap.h                             |  14 +-
 t/perf/p5310-pack-bitmaps.sh              |  68 +-
 t/perf/p5326-multi-pack-bitmaps.sh        |  95 +--
 t/t5310-pack-bitmaps.sh                   | 786 ++++++++++++----------
 t/t5311-pack-bitmaps-shallow.sh           |  53 +-
 t/t5326-multi-pack-bitmaps.sh             | 421 +++++++-----
 t/t5327-multi-pack-bitmaps-rev.sh         |   9 +
 15 files changed, 1244 insertions(+), 657 deletions(-)

base-commit: 39c15e485575089eb77c769f6da02f98a55905e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1266%2FAbhra303%2Fbitmap-commit-table-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1266/Abhra303/bitmap-commit-table-v4
Pull-Request: https://github.com/gitgitgadget/git/pull/1266

Range-diff vs v3:

 1:  f72bf11e6ef = 1:  f72bf11e6ef Documentation/technical: describe bitmap lookup table extension
 2:  5e9b985e39b ! 2:  04244fadf5c pack-bitmap-write.c: write lookup table extension
     @@ Commit message
      
       ## pack-bitmap-write.c ##
      @@ pack-bitmap-write.c: static const struct object_id *oid_access(size_t pos, const void *table)
     - 	return &index[pos]->oid;
     - }
       
     -+static int commit_bitmap_writer_pos(struct object_id *oid,
     -+				    struct pack_idx_entry **index,
     -+				    uint32_t index_nr)
     -+{
     -+	return oid_pos(oid, index, index_nr, oid_access);
     -+}
     -+
       static void write_selected_commits_v1(struct hashfile *f,
       				      struct pack_idx_entry **index,
      -				      uint32_t index_nr)
      +				      uint32_t index_nr,
     -+				      off_t *offsets)
     ++				      off_t *offsets,
     ++				      uint32_t *commit_positions)
       {
       	int i;
       
     -@@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
     + 	for (i = 0; i < writer.selected_nr; ++i) {
       		struct bitmapped_commit *stored = &writer.selected[i];
       
     - 		int commit_pos =
     +-		int commit_pos =
      -			oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
     -+			commit_bitmap_writer_pos(&stored->commit->object.oid, index, index_nr);
     - 
     - 		if (commit_pos < 0)
     - 			BUG("trying to write commit not in index");
     - 
      +		if (offsets)
      +			offsets[i] = hashfile_total(f);
     -+
     - 		hashwrite_be32(f, commit_pos);
     + 
     +-		if (commit_pos < 0)
     +-			BUG("trying to write commit not in index");
     +-
     +-		hashwrite_be32(f, commit_pos);
     ++		hashwrite_be32(f, commit_positions[i]);
       		hashwrite_u8(f, stored->xor_offset);
       		hashwrite_u8(f, stored->flags);
     + 
      @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
       	}
       }
     @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
      +static void write_lookup_table(struct hashfile *f,
      +			       struct pack_idx_entry **index,
      +			       uint32_t index_nr,
     -+			       off_t *offsets)
     ++			       off_t *offsets,
     ++			       uint32_t *commit_positions)
      +{
      +	uint32_t i;
     -+	uint32_t *table, *table_inv, *commit_positions;
     ++	uint32_t *table, *table_inv;
      +
      +	ALLOC_ARRAY(table, writer.selected_nr);
      +	ALLOC_ARRAY(table_inv, writer.selected_nr);
     -+	ALLOC_ARRAY(commit_positions, writer.selected_nr);
     -+
     -+	/* store the index positions of the commits */
     -+	for (i = 0; i < writer.selected_nr; i++) {
     -+		int pos = commit_bitmap_writer_pos(&writer.selected[i].commit->object.oid,
     -+						   index, index_nr);
     -+		if (pos < 0)
     -+			BUG(_("trying to write commit not in index"));
     -+
     -+		commit_positions[i] = pos;
     -+	}
      +
      +	for (i = 0; i < writer.selected_nr; i++)
      +		table[i] = i;
      +
      +	/*
      +	 * At the end of this sort table[j] = i means that the i'th
     -+	 * bitmap corresponds to j'th bitmapped commit in lex order of
     -+	 * OIDs.
     ++	 * bitmap corresponds to j'th bitmapped commit (among the selected
     ++	 * commits) in lex order of OIDs.
      +	 */
      +	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);
      +
     @@ pack-bitmap-write.c: static void write_selected_commits_v1(struct hashfile *f,
      +
      +	free(table);
      +	free(table_inv);
     -+	free(commit_positions);
      +}
      +
       static void write_hash_cache(struct hashfile *f,
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
      +	off_t *offsets = NULL;
       	struct strbuf tmp_file = STRBUF_INIT;
       	struct hashfile *f;
     ++	uint32_t *commit_positions = NULL;
     + 
     + 	struct bitmap_disk_header header;
       
      @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       	dump_bitmap(f, writer.trees);
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       	dump_bitmap(f, writer.tags);
      -	write_selected_commits_v1(f, index, index_nr);
      +
     ++	ALLOC_ARRAY(commit_positions, writer.selected_nr);
     ++	for (uint32_t i = 0; i < writer.selected_nr; ++i) {
     ++		struct bitmapped_commit *stored = &writer.selected[i];
     ++		int commit_pos = oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
     ++
     ++		if (commit_pos < 0)
     ++			BUG(_("trying to write commit not in index"));
     ++
     ++		commit_positions[i] = commit_pos;
     ++	}
     ++
      +	if (options & BITMAP_OPT_LOOKUP_TABLE)
      +		CALLOC_ARRAY(offsets, index_nr);
      +
     -+	write_selected_commits_v1(f, index, index_nr, offsets);
     ++	write_selected_commits_v1(f, index, index_nr, offsets, commit_positions);
      +
      +	if (options & BITMAP_OPT_LOOKUP_TABLE)
     -+		write_lookup_table(f, index, index_nr, offsets);
     ++		write_lookup_table(f, index, index_nr, offsets, commit_positions);
       
       	if (options & BITMAP_OPT_HASH_CACHE)
       		write_hash_cache(f, index, index_nr);
     @@ pack-bitmap-write.c: void bitmap_writer_finish(struct pack_idx_entry **index,
       
       	strbuf_release(&tmp_file);
      +	free(offsets);
     ++	free(commit_positions);
       }
      
       ## pack-bitmap.h ##
 3:  3dc40cc7f73 = 3:  8bd7639e4b9 pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
 4:  e64362621d2 ! 4:  afc8c660ac1 pack-bitmap: prepare to read lookup table extension
     @@ pack-bitmap.c: struct include_data {
      +};
      +
      +/*
     ++ * Given a `triplet` struct pointer and pointer `p`, this
     ++ * function reads the triplet beginning at `p` into the struct.
     ++ * Note that this function assumes that there is enough memory
     ++ * left for filling the `triplet` struct from `p`.
     ++ */
     ++static int lookup_table_get_triplet_by_pointer(struct bitmap_lookup_table_triplet *triplet,
     ++					       const unsigned char *p)
     ++{
     ++	if (!triplet)
     ++		return -1;
     ++
     ++	triplet->commit_pos = get_be32(p);
     ++	p += sizeof(uint32_t);
     ++	triplet->offset = get_be64(p);
     ++	p += sizeof(uint64_t);
     ++	triplet->xor_row = get_be32(p);
     ++	return 0;
     ++}
     ++
     ++/*
      + * This function gets the raw triplet from `row`'th row in the
      + * lookup table and fills that data to the `triplet`.
      + */
     @@ pack-bitmap.c: struct include_data {
      +
      +	p = bitmap_git->table_lookup + st_mult(pos, BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH);
      +
     -+	triplet->commit_pos = get_be32(p);
     -+	p += sizeof(uint32_t);
     -+	triplet->offset = get_be64(p);
     -+	p += sizeof(uint64_t);
     -+	triplet->xor_row = get_be32(p);
     -+	return 0;
     ++	return lookup_table_get_triplet_by_pointer(triplet, p);
      +}
      +
      +/*
     -+ * Searches for a matching triplet. `va` is a pointer
     -+ * to the wanted commit position value. `vb` points to
     ++ * Searches for a matching triplet. `commit_pos` is a pointer
     ++ * to the wanted commit position value. `table_entry` points to
      + * a triplet in lookup table. The first 4 bytes of each
     -+ * triplet (pointed by `vb`) are compared with `*va`.
     ++ * triplet (pointed by `table_entry`) are compared with `*commit_pos`.
      + */
     -+static int triplet_cmp(const void *va, const void *vb)
     ++static int triplet_cmp(const void *commit_pos, const void *table_entry)
      +{
      +
     -+	uint32_t a = *(uint32_t *)va;
     -+	uint32_t b = get_be32(vb);
     ++	uint32_t a = *(uint32_t *)commit_pos;
     ++	uint32_t b = get_be32(table_entry);
      +	if (a > b)
      +		return 1;
      +	else if (a < b)
     @@ pack-bitmap.c: struct include_data {
      +}
      +
      +/*
     -+ * `bsearch_triplet` function searches for the raw triplet having
     -+ * commit position same as `commit_pos` and fills `triplet`
     -+ * object from the raw triplet. Returns 1 on success and 0
     -+ * on failure.
     ++ * `bsearch_triplet_by_pos` function searches for the raw triplet
     ++ * having commit position same as `commit_pos` and fills `triplet`
     ++ * object from the raw triplet. Returns 1 on success and 0 on
     ++ * failure.
      + */
     -+static int bsearch_triplet(uint32_t *commit_pos,
     -+			   struct bitmap_index *bitmap_git,
     -+			   struct bitmap_lookup_table_triplet *triplet)
     ++static int bsearch_triplet_by_pos(uint32_t commit_pos,
     ++				  struct bitmap_index *bitmap_git,
     ++				  struct bitmap_lookup_table_triplet *triplet)
      +{
     -+	unsigned char *p = bsearch(commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
     ++	unsigned char *p = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
      +				   BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH, triplet_cmp);
      +
      +	if (!p)
     -+		return 0;
     -+	triplet->commit_pos = get_be32(p);
     -+	p += sizeof(uint32_t);
     -+	triplet->offset = get_be64(p);
     -+	p += sizeof(uint64_t);
     -+	triplet->xor_row = get_be32(p);
     -+	return 1;
     ++		return -1;
     ++
     ++	return lookup_table_get_triplet_by_pointer(triplet, p);
      +}
      +
      +static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
     @@ pack-bitmap.c: struct include_data {
      +{
      +	uint32_t commit_pos, xor_row;
      +	uint64_t offset;
     -+	int flags;
     ++	int flags, found;
      +	struct bitmap_lookup_table_triplet triplet;
      +	struct object_id *oid = &commit->object.oid;
      +	struct ewah_bitmap *bitmap;
      +	struct stored_bitmap *xor_bitmap = NULL;
     ++	const int bitmap_header_size = 6;
     ++	static struct bitmap_lookup_table_xor_item *xor_items = NULL;
     ++	static size_t xor_items_nr = 0, xor_items_alloc = 0;
     ++	static int is_corrupt = 0;
     ++
     ++	if (is_corrupt)
     ++		return NULL;
      +
     -+	int found = bsearch_pos(bitmap_git, oid, &commit_pos);
     ++	found = bsearch_pos(bitmap_git, oid, &commit_pos);
      +
      +	if (!found)
      +		return NULL;
      +
     -+	if (!bsearch_triplet(&commit_pos, bitmap_git, &triplet))
     ++	if (bsearch_triplet_by_pos(commit_pos, bitmap_git, &triplet) < 0)
      +		return NULL;
      +
     ++	xor_items_nr = 0;
      +	offset = triplet.offset;
      +	xor_row = triplet.xor_row;
      +
     @@ pack-bitmap.c: struct include_data {
      +		int xor_flags;
      +		khiter_t hash_pos;
      +		uint64_t offset_xor;
     -+		struct bitmap_lookup_table_xor_item *xor_items;
     -+		struct bitmap_lookup_table_xor_item xor_item;
     -+		size_t xor_items_nr = 0, xor_items_alloc = 64;
     ++		struct bitmap_lookup_table_xor_item *xor_item;
      +
     -+		ALLOC_ARRAY(xor_items, xor_items_alloc);
      +		while (xor_row != 0xffffffff) {
     -+			struct object_id xor_oid;
     ++			ALLOC_GROW(xor_items, xor_items_nr + 1, xor_items_alloc);
      +
      +			if (xor_items_nr + 1 >= bitmap_git->entry_count) {
     -+				free(xor_items);
      +				error(_("corrupt bitmap lookup table: xor chain exceed entry count"));
     -+				return NULL;
     ++				goto corrupt;
      +			}
      +
      +			if (lookup_table_get_triplet(bitmap_git, xor_row, &triplet) < 0)
     -+				return NULL;
     ++				goto corrupt;
      +
     -+			offset_xor = triplet.offset;
     ++			xor_item = &xor_items[xor_items_nr];
     ++			xor_item->offset = triplet.offset;
      +
     -+			if (nth_bitmap_object_oid(bitmap_git, &xor_oid, triplet.commit_pos) < 0) {
     -+				free(xor_items);
     ++			if (nth_bitmap_object_oid(bitmap_git, &xor_item->oid, triplet.commit_pos) < 0) {
      +				error(_("corrupt bitmap lookup table: commit index %u out of range"),
      +					triplet.commit_pos);
     -+				return NULL;
     ++				goto corrupt;
      +			}
      +
     -+			hash_pos = kh_get_oid_map(bitmap_git->bitmaps, xor_oid);
     ++			hash_pos = kh_get_oid_map(bitmap_git->bitmaps, xor_item->oid);
      +
      +			/*
      +			 * If desired bitmap is already stored, we don't need
     @@ pack-bitmap.c: struct include_data {
      +			if (hash_pos < kh_end(bitmap_git->bitmaps) &&
      +			    (xor_bitmap = kh_value(bitmap_git->bitmaps, hash_pos)))
      +				break;
     -+
     -+			ALLOC_GROW(xor_items, xor_items_nr + 1, xor_items_alloc);
     -+			xor_items[xor_items_nr++] = (struct bitmap_lookup_table_xor_item) {.oid = xor_oid,
     -+											   .offset = offset_xor};
     ++			xor_items_nr++;
      +			xor_row = triplet.xor_row;
      +		}
      +
      +		while (xor_items_nr) {
     -+			xor_item = xor_items[xor_items_nr - 1];
     -+			offset_xor = xor_item.offset;
     ++			xor_item = &xor_items[xor_items_nr - 1];
     ++			offset_xor = xor_item->offset;
      +
      +			bitmap_git->map_pos = offset_xor;
     -+			if (bitmap_git->map_size - bitmap_git->map_pos < 6) {
     ++			if (bitmap_git->map_size - bitmap_git->map_pos < bitmap_header_size) {
      +				error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
     -+					oid_to_hex(&xor_item.oid));
     -+				free(xor_items);
     -+				return NULL;
     ++					oid_to_hex(&xor_item->oid));
     ++				goto corrupt;
      +			}
      +
      +			bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
      +			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
      +			bitmap = read_bitmap_1(bitmap_git);
      +
     -+			if (!bitmap) {
     -+				free(xor_items);
     -+				return NULL;
     -+			}
     ++			if (!bitmap)
     ++				goto corrupt;
      +
     -+			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_item.oid, xor_bitmap, xor_flags);
     ++			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_item->oid, xor_bitmap, xor_flags);
      +			xor_items_nr--;
      +		}
     -+
     -+		free(xor_items);
      +	}
      +
      +	bitmap_git->map_pos = offset;
     -+	if (bitmap_git->map_size - bitmap_git->map_pos < 6) {
     ++	if (bitmap_git->map_size - bitmap_git->map_pos < bitmap_header_size) {
      +		error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
      +			oid_to_hex(oid));
     -+		return NULL;
     ++		goto corrupt;
      +	}
      +
      +	bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
     @@ pack-bitmap.c: struct include_data {
      +	bitmap = read_bitmap_1(bitmap_git);
      +
      +	if (!bitmap)
     -+		return NULL;
     ++		goto corrupt;
      +
      +	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);
     ++
     ++corrupt:
     ++	free(xor_items);
     ++	is_corrupt = 1;
     ++	return NULL;
      +}
      +
       struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,

6: 4f9f1049485 ! 5: fc69489e395 p5310-pack-bitmaps.sh: remove pack.writeReverseIndex
@@ Metadata
Author: Abhradeep Chakraborty <[email protected]>

## Commit message ##
- p5310-pack-bitmaps.sh: remove pack.writeReverseIndex
+ p5310-pack-bitmaps.sh: enable pack.writeReverseIndex

- The previous change enables the pack.writereverseindex to see
- the effect of writing reverse index in the performance test.
+ Enable pack.writeReverseIndex before running pack-bitmap related
+ performance tests.

- Remove the pack.writeReverseIndex configuration.
+ The performance difference with pack.writeReverseIndex enabled and
+ with disabled are given below -

- Below is the result of performance test. Output format is in
- seconds.
+ With pack.writeReverseIndex
+ -------------------------------
+
+ Test this tree
+ -------------------------------------------------------------------------
+ 5310.3: repack to disk 296.55(256.53+14.52)
+ 5310.4: simulated clone 15.64(8.88+1.39)
+ 5310.5: simulated fetch 1.65(2.75+0.20)
+ 5310.6: pack to file (bitmap) 48.71(30.20+7.58)
+ 5310.7: rev-list (commits) 0.61(0.41+0.08)
+ 5310.8: rev-list (objects) 4.38(4.26+0.09)
+ 5310.9: rev-list with tag negated via --not 0.07(0.02+0.04)
+ --all (objects)
+ 5310.10: rev-list with negative tag (objects) 0.05(0.01+0.03)
+ 5310.11: rev-list count with blob:none 0.08(0.03+0.04)
+ 5310.12: rev-list count with blob:limit=1k 7.29(6.92+0.30)
+ 5310.13: rev-list count with tree:0 0.08(0.03+0.04)
+ 5310.14: simulated partial clone 9.45(8.12+0.41)
+ 5310.16: clone (partial bitmap) 17.02(10.61+2.67)
+ 5310.17: pack to file (partial bitmap) 51.91(28.57+7.48)
+ 5310.18: rev-list with tree filter (partial bitmap) 1.00(0.22+0.24)
+
+ Without pack.writeReverseIndex:
+ -----------------------------

Test this tree
------------------------------------------------------------------------
- 5310.4: repack to disk (lookup=false) 293.80(251.30+14.30)
- 5310.5: simulated clone 12.50(5.15+1.36)
- 5310.6: simulated fetch 1.83(2.90+0.23)
- 5310.7: pack to file (bitmap) 39.70(20.25+7.14)
- 5310.8: rev-list (commits) 1.00(0.60+0.13)
- 5310.9: rev-list (objects) 4.11(4.00+0.10)
- 5310.10: rev-list with tag negated via --not 0.07(0.02+0.05)
+ 5310.3: repack to disk 293.80(251.30+14.30)
+ 5310.4: simulated clone 12.50(5.15+1.36)
+ 5310.5: simulated fetch 1.83(2.90+0.23)
+ 5310.6: pack to file (bitmap) 39.70(20.25+7.14)
+ 5310.7: rev-list (commits) 1.00(0.60+0.13)
+ 5310.8: rev-list (objects) 4.11(4.00+0.10)
+ 5310.9: rev-list with tag negated via --not 0.07(0.02+0.05)
--all (objects)
- 5310.11: rev-list with negative tag (objects) 0.23(0.16+0.06)
- 5310.12: rev-list count with blob:none 0.27(0.18+0.08)
- 5310.13: rev-list count with blob:limit=1k 6.41(5.98+0.41)
- 5310.14: rev-list count with tree:0 0.26(0.18+0.07)
- 5310.15: simulated partial clone 4.34(3.29+0.37)
- 5310.19: repack to disk (lookup=true) 250.93(171.97+20.78)
- 5310.20: simulated clone 10.80(5.14+1.06)
- 5310.21: simulated fetch 0.71(0.79+0.16)
- 5310.22: pack to file (bitmap) 39.49(20.19+6.98)
- 5310.23: rev-list (commits) 0.81(0.48+0.09)
- 5310.24: rev-list (objects) 3.48(3.38+0.09)
- 5310.25: rev-list with tag negated via --not 0.04(0.00+0.03)
- --all (objects)
- 5310.26: rev-list with negative tag (objects) 0.22(0.16+0.05)
- 5310.27: rev-list count with blob:none 0.22(0.16+0.05)
- 5310.28: rev-list count with blob:limit=1k 6.21(5.76+0.29)
- 5310.29: rev-list count with tree:0 0.23(0.16+0.06)
- 5310.30: simulated partial clone 4.53(3.14+0.39)
-
- Tests 4-15 are without the use of lookup table. The rests are
- repeatation of the previous tests but using lookup table.
+ 5310.10: rev-list with negative tag (objects) 0.23(0.16+0.06)
+ 5310.11: rev-list count with blob:none 0.27(0.18+0.08)
+ 5310.12: rev-list count with blob:limit=1k 6.41(5.98+0.41)
+ 5310.13: rev-list count with tree:0 0.26(0.18+0.07)
+ 5310.14: simulated partial clone 4.34(3.29+0.37)
+ 5310.16: clone (partial bitmap) 21.48(15.12+2.42)
+ 5310.17: pack to file (partial bitmap) 47.35(37.80+4.84)
+ 5310.18: rev-list with tree filter (partial bitmap) 0.73(0.07+0.21)

- Mentored-by: Taylor Blau <[email protected]>
- Co-Mentored-by: Kaartic Sivaraam <[email protected]>

## t/perf/p5310-pack-bitmaps.sh ##
@@ t/perf/p5310-pack-bitmaps.sh: test_perf_large_repo
# We intentionally use the deprecated pack.writebitmaps # config so that we can test against older versions of git. test_expect_success 'setup bitmap config' '
-- git config pack.writebitmaps true &&
-- git config pack.writeReverseIndex true
-+ git config pack.writebitmaps true
+- git config pack.writebitmaps true
++ git config pack.writebitmaps true &&
++ git config pack.writeReverseIndex true
'

- test_bitmap () {
+ # we need to create the tag up front such that it is covered by the repack and
5: a155c1e2eba ! 6: 52f7d8359ee bitmap-lookup-table: add performance tests for lookup table
@@ Commit message
5310.13: rev-list count with blob:limit=1k 7.29(6.92+0.30)
5310.14: rev-list count with tree:0 0.08(0.03+0.04)
5310.15: simulated partial clone 9.45(8.12+0.41)
- 5310.19: repack to disk (lookup=true) 255.92(188.13+20.47)
- 5310.20: simulated clone 13.78(8.84+1.09)
- 5310.21: simulated fetch 0.52(0.63+0.14)
- 5310.22: pack to file (bitmap) 44.34(28.94+6.84)
- 5310.23: rev-list (commits) 0.48(0.31+0.06)
- 5310.24: rev-list (objects) 4.02(3.93+0.07)
- 5310.25: rev-list with tag negated via --not 0.04(0.00+0.03)
+ 5310.17: clone (partial bitmap) 21.00(15.04+2.39)
+ 5310.18: pack to file (partial bitmap) 47.98(38.13+5.23)
+ 5310.19: rev-list with tree filter (partial bitmap) 0.70(0.07+0.20)
+ 5310.22: repack to disk (lookup=true) 255.92(188.13+20.47)
+ 5310.23: simulated clone 13.78(8.84+1.09)
+ 5310.24: simulated fetch 0.52(0.63+0.14)
+ 5310.25: pack to file (bitmap) 44.34(28.94+6.84)
+ 5310.26: rev-list (commits) 0.48(0.31+0.06)
+ 5310.27: rev-list (objects) 4.02(3.93+0.07)
+ 5310.28: rev-list with tag negated via --not 0.04(0.00+0.03)
--all (objects)
- 5310.26: rev-list with negative tag (objects) 0.04(0.00+0.03)
- 5310.27: rev-list count with blob:none 0.04(0.01+0.03)
- 5310.28: rev-list count with blob:limit=1k 6.48(6.23+0.22)
- 5310.29: rev-list count with tree:0 0.04(0.01+0.03)
- 5310.30: simulated partial clone 8.30(7.21+0.36)
+ 5310.29: rev-list with negative tag (objects) 0.04(0.00+0.03)
+ 5310.30: rev-list count with blob:none 0.04(0.01+0.03)
+ 5310.31: rev-list count with blob:limit=1k 6.48(6.23+0.22)
+ 5310.32: rev-list count with tree:0 0.04(0.01+0.03)
+ 5310.33: simulated partial clone 8.30(7.21+0.36)
+ 5310.35: clone (partial bitmap) 20.34(15.00+2.41)
+ 5310.36: pack to file (partial bitmap) 46.45(38.05+5.20)
+ 5310.37: rev-list with tree filter (partial bitmap) 0.61(0.06+0.20)

Test 4-15 are tested without using lookup table. Same tests are repeated in 16-30 (using lookup table).
@@ Commit message

## t/perf/p5310-pack-bitmaps.sh ##
-@@ t/perf/p5310-pack-bitmaps.sh: test_perf_large_repo
- # We intentionally use the deprecated pack.writebitmaps
- # config so that we can test against older versions of git.
- test_expect_success 'setup bitmap config' '
-- git config pack.writebitmaps true
-+ git config pack.writebitmaps true &&
-+ git config pack.writeReverseIndex true
+@@ t/perf/p5310-pack-bitmaps.sh: test_expect_success 'setup bitmap config' '
+ git config pack.writeReverseIndex true
'

-# we need to create the tag up front such that it is covered by the repack and
@@ t/perf/p5310-pack-bitmaps.sh: test_perf_large_repo
+ # had happened
+ git update-ref HEAD $orig_tip
+ '
++
++ test_partial_bitmap
+}

-test_partial_bitmap
@@ t/perf/p5326-multi-pack-bitmaps.sh: test_description='Tests performance using mi
+ # had happened
+ git update-ref HEAD $orig_tip
+ '
++
++ test_partial_bitmap
+}
+
+test_bitmap false

Signature · 1 line
gitgitgadget
#108 lore raw

PATCH v4 4/6 pack-bitmap: prepare to read lookup table extension

From: Abhradeep Chakraborty <[email protected]>

Earlier change teaches Git to write bitmap lookup table. But Git does not know how to parse them.

Teach Git to parse the existing bitmap lookup table. The older versions of Git are not affected by it. Those versions ignore the lookup table.

---
 pack-bitmap.c           | 278 ++++++++++++++++++++++++++++++++++++++--
 pack-bitmap.h           |   9 ++
 t/t5310-pack-bitmaps.sh |  22 ++++
 3 files changed, 299 insertions(+), 10 deletions(-)
pack-bitmap.c +268 -10
index 36134222d7a..7c66d4379f5 100644
@@ -82,6 +82,12 @@ struct bitmap_index {
 	/* The checksum of the packfile or MIDX; points into map. */
 	const unsigned char *checksum;
 
+	/*
+	 * If not NULL, this point into the commit table extension
+	 * (within the memory mapped region `map`).
+	 */
+	unsigned char *table_lookup;
+
 	/*
 	 * Extended index.
 	 *
@@ -185,6 +191,16 @@ static int load_bitmap_header(struct bitmap_index *index)
 			index->hashes = (void *)(index_end - cache_size);
 			index_end -= cache_size;
 		}
+
+		if (flags & BITMAP_OPT_LOOKUP_TABLE) {
+			size_t table_size = st_mult(ntohl(header->entry_count),
+						    BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH);
+			if (table_size > index_end - index->map - header_size)
+				return error(_("corrupted bitmap index file (too short to fit lookup table)"));
+			if (git_env_bool("GIT_TEST_READ_COMMIT_TABLE", 1))
+				index->table_lookup = (void *)(index_end - table_size);
+			index_end -= table_size;
+		}
 	}
 
 	index->entry_count = ntohl(header->entry_count);
@@ -211,11 +227,13 @@ static struct stored_bitmap *store_bitmap(struct bitmap_index *index,
 
 	hash_pos = kh_put_oid_map(index->bitmaps, stored->oid, &ret);
 
-	/* a 0 return code means the insertion succeeded with no changes,
-	 * because the SHA1 already existed on the map. this is bad, there
-	 * shouldn't be duplicated commits in the index */
+	/*
+	 * A 0 return code means the insertion succeeded with no changes,
+	 * because the SHA1 already existed on the map. This is bad, there
+	 * shouldn't be duplicated commits in the index.
+	 */
 	if (ret == 0) {
-		error("Duplicate entry in bitmap index: %s", oid_to_hex(oid));
+		error(_("duplicate entry in bitmap index: %s"), oid_to_hex(oid));
 		return NULL;
 	}
 
@@ -470,7 +488,7 @@ static int load_bitmap(struct bitmap_index *bitmap_git)
 		!(bitmap_git->tags = read_bitmap_1(bitmap_git)))
 		goto failed;
 
-	if (load_bitmap_entries_v1(bitmap_git) < 0)
+	if (!bitmap_git->table_lookup && load_bitmap_entries_v1(bitmap_git) < 0)
 		goto failed;
 
 	return 0;
@@ -557,13 +575,241 @@ struct include_data {
 	struct bitmap *seen;
 };
 
+struct bitmap_lookup_table_triplet {
+	uint32_t commit_pos;
+	uint64_t offset;
+	uint32_t xor_row;
+};
+
+struct bitmap_lookup_table_xor_item {
+	struct object_id oid;
+	uint64_t offset;
+};
+
+/*
+ * Given a `triplet` struct pointer and pointer `p`, this
+ * function reads the triplet beginning at `p` into the struct.
+ * Note that this function assumes that there is enough memory
+ * left for filling the `triplet` struct from `p`.
+ */
+static int lookup_table_get_triplet_by_pointer(struct bitmap_lookup_table_triplet *triplet,
+					       const unsigned char *p)
+{
+	if (!triplet)
+		return -1;
+
+	triplet->commit_pos = get_be32(p);
+	p += sizeof(uint32_t);
+	triplet->offset = get_be64(p);
+	p += sizeof(uint64_t);
+	triplet->xor_row = get_be32(p);
+	return 0;
+}
+
+/*
+ * This function gets the raw triplet from `row`'th row in the
+ * lookup table and fills that data to the `triplet`.
+ */
+static int lookup_table_get_triplet(struct bitmap_index *bitmap_git,
+				    uint32_t pos,
+				    struct bitmap_lookup_table_triplet *triplet)
+{
+	unsigned char *p = NULL;
+	if (pos >= bitmap_git->entry_count)
+		return error(_("corrupt bitmap lookup table: triplet position out of index"));
+
+	p = bitmap_git->table_lookup + st_mult(pos, BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH);
+
+	return lookup_table_get_triplet_by_pointer(triplet, p);
+}
+
+/*
+ * Searches for a matching triplet. `commit_pos` is a pointer
+ * to the wanted commit position value. `table_entry` points to
+ * a triplet in lookup table. The first 4 bytes of each
+ * triplet (pointed by `table_entry`) are compared with `*commit_pos`.
+ */
+static int triplet_cmp(const void *commit_pos, const void *table_entry)
+{
+
+	uint32_t a = *(uint32_t *)commit_pos;
+	uint32_t b = get_be32(table_entry);
+	if (a > b)
+		return 1;
+	else if (a < b)
+		return -1;
+
+	return 0;
+}
+
+static uint32_t bsearch_pos(struct bitmap_index *bitmap_git,
+			    struct object_id *oid,
+			    uint32_t *result)
+{
+	int found;
+
+	if (bitmap_is_midx(bitmap_git))
+		found = bsearch_midx(oid, bitmap_git->midx, result);
+	else
+		found = bsearch_pack(oid, bitmap_git->pack, result);
+
+	return found;
+}
+
+/*
+ * `bsearch_triplet_by_pos` function searches for the raw triplet
+ * having commit position same as `commit_pos` and fills `triplet`
+ * object from the raw triplet. Returns 1 on success and 0 on
+ * failure.
+ */
+static int bsearch_triplet_by_pos(uint32_t commit_pos,
+				  struct bitmap_index *bitmap_git,
+				  struct bitmap_lookup_table_triplet *triplet)
+{
+	unsigned char *p = bsearch(&commit_pos, bitmap_git->table_lookup, bitmap_git->entry_count,
+				   BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH, triplet_cmp);
+
+	if (!p)
+		return -1;
+
+	return lookup_table_get_triplet_by_pointer(triplet, p);
+}
+
+static struct stored_bitmap *lazy_bitmap_for_commit(struct bitmap_index *bitmap_git,
+					  struct commit *commit)
+{
+	uint32_t commit_pos, xor_row;
+	uint64_t offset;
+	int flags, found;
+	struct bitmap_lookup_table_triplet triplet;
+	struct object_id *oid = &commit->object.oid;
+	struct ewah_bitmap *bitmap;
+	struct stored_bitmap *xor_bitmap = NULL;
+	const int bitmap_header_size = 6;
+	static struct bitmap_lookup_table_xor_item *xor_items = NULL;
+	static size_t xor_items_nr = 0, xor_items_alloc = 0;
+	static int is_corrupt = 0;
+
+	if (is_corrupt)
+		return NULL;
+
+	found = bsearch_pos(bitmap_git, oid, &commit_pos);
+
+	if (!found)
+		return NULL;
+
+	if (bsearch_triplet_by_pos(commit_pos, bitmap_git, &triplet) < 0)
+		return NULL;
+
+	xor_items_nr = 0;
+	offset = triplet.offset;
+	xor_row = triplet.xor_row;
+
+	if (xor_row != 0xffffffff) {
+		int xor_flags;
+		khiter_t hash_pos;
+		uint64_t offset_xor;
+		struct bitmap_lookup_table_xor_item *xor_item;
+
+		while (xor_row != 0xffffffff) {
+			ALLOC_GROW(xor_items, xor_items_nr + 1, xor_items_alloc);
+
+			if (xor_items_nr + 1 >= bitmap_git->entry_count) {
+				error(_("corrupt bitmap lookup table: xor chain exceed entry count"));
+				goto corrupt;
+			}
+
+			if (lookup_table_get_triplet(bitmap_git, xor_row, &triplet) < 0)
+				goto corrupt;
+
+			xor_item = &xor_items[xor_items_nr];
+			xor_item->offset = triplet.offset;
+
+			if (nth_bitmap_object_oid(bitmap_git, &xor_item->oid, triplet.commit_pos) < 0) {
+				error(_("corrupt bitmap lookup table: commit index %u out of range"),
+					triplet.commit_pos);
+				goto corrupt;
+			}
+
+			hash_pos = kh_get_oid_map(bitmap_git->bitmaps, xor_item->oid);
+
+			/*
+			 * If desired bitmap is already stored, we don't need
+			 * to iterate further. Because we know that bitmaps
+			 * that are needed to be parsed to parse this bitmap
+			 * has already been stored. So, assign this stored bitmap
+			 * to the xor_bitmap.
+			 */
+			if (hash_pos < kh_end(bitmap_git->bitmaps) &&
+			    (xor_bitmap = kh_value(bitmap_git->bitmaps, hash_pos)))
+				break;
+			xor_items_nr++;
+			xor_row = triplet.xor_row;
+		}
+
+		while (xor_items_nr) {
+			xor_item = &xor_items[xor_items_nr - 1];
+			offset_xor = xor_item->offset;
+
+			bitmap_git->map_pos = offset_xor;
+			if (bitmap_git->map_size - bitmap_git->map_pos < bitmap_header_size) {
+				error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
+					oid_to_hex(&xor_item->oid));
+				goto corrupt;
+			}
+
+			bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
+			xor_flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+			bitmap = read_bitmap_1(bitmap_git);
+
+			if (!bitmap)
+				goto corrupt;
+
+			xor_bitmap = store_bitmap(bitmap_git, bitmap, &xor_item->oid, xor_bitmap, xor_flags);
+			xor_items_nr--;
+		}
+	}
+
+	bitmap_git->map_pos = offset;
+	if (bitmap_git->map_size - bitmap_git->map_pos < bitmap_header_size) {
+		error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
+			oid_to_hex(oid));
+		goto corrupt;
+	}
+
+	bitmap_git->map_pos = bitmap_git->map_pos + sizeof(uint32_t) + sizeof(uint8_t);
+	flags = read_u8(bitmap_git->map, &bitmap_git->map_pos);
+	bitmap = read_bitmap_1(bitmap_git);
+
+	if (!bitmap)
+		goto corrupt;
+
+	return store_bitmap(bitmap_git, bitmap, oid, xor_bitmap, flags);
+
+corrupt:
+	free(xor_items);
+	is_corrupt = 1;
+	return NULL;
+}
+
 struct ewah_bitmap *bitmap_for_commit(struct bitmap_index *bitmap_git,
 				      struct commit *commit)
 {
 	khiter_t hash_pos = kh_get_oid_map(bitmap_git->bitmaps,
 					   commit->object.oid);
-	if (hash_pos >= kh_end(bitmap_git->bitmaps))
-		return NULL;
+	if (hash_pos >= kh_end(bitmap_git->bitmaps)) {
+		struct stored_bitmap *bitmap = NULL;
+		if (!bitmap_git->table_lookup)
+			return NULL;
+
+		trace2_region_enter("pack-bitmap", "reading_lookup_table", the_repository);
+		/* NEEDSWORK: cache misses aren't recorded */
+		bitmap = lazy_bitmap_for_commit(bitmap_git, commit);
+		trace2_region_leave("pack-bitmap", "reading_lookup_table", the_repository);
+		if (!bitmap)
+			return NULL;
+		return lookup_stored_bitmap(bitmap);
+	}
 	return lookup_stored_bitmap(kh_value(bitmap_git->bitmaps, hash_pos));
 }
 
@@ -1699,8 +1945,10 @@ void test_bitmap_walk(struct rev_info *revs)
 	if (revs->pending.nr != 1)
 		die("you must specify exactly one commit to test");
 
-	fprintf(stderr, "Bitmap v%d test (%d entries loaded)\n",
-		bitmap_git->version, bitmap_git->entry_count);
+	fprintf(stderr, "Bitmap v%d test (%d entries%s)",
+		bitmap_git->version,
+		bitmap_git->entry_count,
+		bitmap_git->table_lookup ? "" : " loaded");
 
 	root = revs->pending.objects[0].item;
 	bm = bitmap_for_commit(bitmap_git, (struct commit *)root);
@@ -1753,13 +2001,23 @@ void test_bitmap_walk(struct rev_info *revs)
 
 int test_bitmap_commits(struct repository *r)
 {
-	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
 	struct object_id oid;
 	MAYBE_UNUSED void *value;
+	struct bitmap_index *bitmap_git = prepare_bitmap_git(r);
+
+	/*
+	 * As this function is only used to print bitmap selected
+	 * commits, we don't have to read the commit table.
+	 */
 
 	if (!bitmap_git)
 		die("failed to load bitmap indexes");
 
+	if (bitmap_git->table_lookup) {
+		if (load_bitmap_entries_v1(bitmap_git) < 0)
+			die(_("failed to load bitmap indexes"));
+	}
+
 	kh_foreach(bitmap_git->bitmaps, oid, value, {
 		printf("%s\n", oid_to_hex(&oid));
 	});
pack-bitmap.h +9 -0
index 67a9d0fc303..9278f71ac91 100644
@@ -23,6 +23,15 @@ struct bitmap_disk_header {
 
 #define NEEDS_BITMAP (1u<<22)
 
+/*
+ * The width in bytes of a single triplet in the lookup table
+ * extension:
+ *     (commit_pos, offset, xor_row)
+ *
+ * whose fields ar 32-, 64-, 32- bits wide, respectively.
+ */
+#define BITMAP_LOOKUP_TABLE_TRIPLET_WIDTH (16)
+
 enum pack_bitmap_opts {
 	BITMAP_OPT_FULL_DAG = 0x1,
 	BITMAP_OPT_HASH_CACHE = 0x4,
t/t5310-pack-bitmaps.sh +22 -0
index c0607172827..7e50f8e7653 100755
@@ -258,6 +258,7 @@ test_bitmap_cases () {
 
 	test_expect_success 'truncated bitmap fails gracefully (ewah)' '
 		test_config pack.writebitmaphashcache false &&
+		test_config pack.writebitmaplookuptable false &&
 		git repack -ad &&
 		git rev-list --use-bitmap-index --count --all >expect &&
 		bitmap=$(ls .git/objects/pack/*.bitmap) &&
@@ -270,6 +271,7 @@ test_bitmap_cases () {
 	'
 
 	test_expect_success 'truncated bitmap fails gracefully (cache)' '
+		git config pack.writeBitmapLookupTable '"$writeLookupTable"' &&
 		git repack -ad &&
 		git rev-list --use-bitmap-index --count --all >expect &&
 		bitmap=$(ls .git/objects/pack/*.bitmap) &&
@@ -453,4 +455,24 @@ test_expect_success 'verify writing bitmap lookup table when enabled' '
 	grep "\"label\":\"writing_lookup_table\"" trace2
 '
 
+test_expect_success 'lookup table is actually used to traverse objects' '
+	git repack -adb &&
+	GIT_TRACE2_EVENT="$(pwd)/trace3" \
+		git rev-list --use-bitmap-index --count --all &&
+	grep "\"label\":\"reading_lookup_table\"" trace3
+'
+
+test_expect_success 'truncated bitmap fails gracefully (lookup table)' '
+	test_config pack.writebitmaphashcache false &&
+	git repack -adb &&
+	git rev-list --use-bitmap-index --count --all >expect &&
+	bitmap=$(ls .git/objects/pack/*.bitmap) &&
+	test_when_finished "rm -f $bitmap" &&
+	test_copy_bytes 512 <$bitmap >$bitmap.tmp &&
+	mv -f $bitmap.tmp $bitmap &&
+	git rev-list --use-bitmap-index --count --all >actual 2>stderr &&
+	test_cmp expect actual &&
+	test_i18ngrep corrupted.bitmap.index stderr
+'
+
 test_done
Signature · 1 line
gitgitgadget
#109 lore raw

PATCH v4 5/6 p5310-pack-bitmaps.sh: enable `pack.writeReverseIndex`

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Enable pack.writeReverseIndex before running pack-bitmap related performance tests.

The performance difference with pack.writeReverseIndex enabled and with disabled are given below -

With pack.writeReverseIndex
-------------------------------

Test this tree
-------------------------------------------------------------------------

5310.3: repack to disk                                 296.55(256.53+14.52)
5310.4: simulated clone                                15.64(8.88+1.39)
5310.5: simulated fetch                                1.65(2.75+0.20)
5310.6: pack to file (bitmap)                          48.71(30.20+7.58)
5310.7: rev-list (commits)                             0.61(0.41+0.08)
5310.8: rev-list (objects)                             4.38(4.26+0.09)
5310.9: rev-list with tag negated via --not            0.07(0.02+0.04)
         --all (objects)
5310.10: rev-list with negative tag (objects)          0.05(0.01+0.03)
5310.11: rev-list count with blob:none                 0.08(0.03+0.04)
5310.12: rev-list count with blob:limit=1k             7.29(6.92+0.30)
5310.13: rev-list count with tree:0                    0.08(0.03+0.04)
5310.14: simulated partial clone                       9.45(8.12+0.41)
5310.16: clone (partial bitmap)                        17.02(10.61+2.67)
5310.17: pack to file (partial bitmap)                 51.91(28.57+7.48)
5310.18: rev-list with tree filter (partial bitmap)    1.00(0.22+0.24)

Without pack.writeReverseIndex:
-----------------------------

Test this tree
------------------------------------------------------------------------

5310.3: repack to disk                              293.80(251.30+14.30)
5310.4: simulated clone                             12.50(5.15+1.36)
5310.5: simulated fetch                             1.83(2.90+0.23)
5310.6: pack to file (bitmap)                       39.70(20.25+7.14)
5310.7: rev-list (commits)                          1.00(0.60+0.13)
5310.8: rev-list (objects)                          4.11(4.00+0.10)
5310.9: rev-list with tag negated via --not         0.07(0.02+0.05)
         --all (objects)
5310.10: rev-list with negative tag (objects)       0.23(0.16+0.06)
5310.11: rev-list count with blob:none              0.27(0.18+0.08)
5310.12: rev-list count with blob:limit=1k          6.41(5.98+0.41)
5310.13: rev-list count with tree:0                 0.26(0.18+0.07)
5310.14: simulated partial clone                    4.34(3.29+0.37)
5310.16: clone (partial bitmap)                     21.48(15.12+2.42)
5310.17: pack to file (partial bitmap)              47.35(37.80+4.84)

5310.18: rev-list with tree filter (partial bitmap) 0.73(0.07+0.21)

---
 t/perf/p5310-pack-bitmaps.sh | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
t/perf/p5310-pack-bitmaps.sh +2 -1
index 7ad4f237bc3..6e8abcd5b21 100755
@@ -13,7 +13,8 @@ test_perf_large_repo
 # We intentionally use the deprecated pack.writebitmaps
 # config so that we can test against older versions of git.
 test_expect_success 'setup bitmap config' '
-	git config pack.writebitmaps true
+	git config pack.writebitmaps true &&
+	git config pack.writeReverseIndex true
 '
 
 # we need to create the tag up front such that it is covered by the repack and
Signature · 1 line
gitgitgadget
#107 lore raw

PATCH v4 3/6 pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Teach Git to provide a way for users to enable/disable bitmap lookup table extension by providing a config option named 'writeBitmapLookupTable'. Default is false.

Also add test to verify writting of lookup table.

---
 Documentation/config/pack.txt     |   7 +
 builtin/multi-pack-index.c        |   7 +
 builtin/pack-objects.c            |   8 +
 midx.c                            |   3 +
 midx.h                            |   1 +
 t/t5310-pack-bitmaps.sh           | 792 ++++++++++++++++--------------
 t/t5311-pack-bitmaps-shallow.sh   |  53 +-
 t/t5326-multi-pack-bitmaps.sh     | 421 +++++++++-------
 t/t5327-multi-pack-bitmaps-rev.sh |   9 +
 9 files changed, 720 insertions(+), 581 deletions(-)
Documentation/config/pack.txt +7 -0
index ad7f73a1ead..b955ca572ec 100644
@@ -164,6 +164,13 @@ When writing a multi-pack reachability bitmap, no new namehashes are
 computed; instead, any namehashes stored in an existing bitmap are
 permuted into their appropriate location when writing a new bitmap.
 
+pack.writeBitmapLookupTable::
+	When true, Git will include a "lookup table" section in the
+	bitmap index (if one is written). This table is used to defer
+	loading individual bitmaps as late as possible. This can be
+	beneficial in repositories that have relatively large bitmap
+	indexes. Defaults to false.
+
 pack.writeReverseIndex::
 	When true, git will write a corresponding .rev file (see:
 	link:../technical/pack-format.html[Documentation/technical/pack-format.txt])
builtin/multi-pack-index.c +7 -0
index 5edbb7fe86e..55402b46f41 100644
@@ -87,6 +87,13 @@ static int git_multi_pack_index_write_config(const char *var, const char *value,
 			opts.flags &= ~MIDX_WRITE_BITMAP_HASH_CACHE;
 	}
 
+	if (!strcmp(var, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(var, value))
+			opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+		else
+			opts.flags &= ~MIDX_WRITE_BITMAP_LOOKUP_TABLE;
+	}
+
 	/*
 	 * We should never make a fall-back call to 'git_default_config', since
 	 * this was already called in 'cmd_multi_pack_index()'.
builtin/pack-objects.c +8 -0
index 39e28cfcafc..46e26774963 100644
@@ -3148,6 +3148,14 @@ static int git_pack_config(const char *k, const char *v, void *cb)
 		else
 			write_bitmap_options &= ~BITMAP_OPT_HASH_CACHE;
 	}
+
+	if (!strcmp(k, "pack.writebitmaplookuptable")) {
+		if (git_config_bool(k, v))
+			write_bitmap_options |= BITMAP_OPT_LOOKUP_TABLE;
+		else
+			write_bitmap_options &= ~BITMAP_OPT_LOOKUP_TABLE;
+	}
+
 	if (!strcmp(k, "pack.usebitmaps")) {
 		use_bitmap_index_default = git_config_bool(k, v);
 		return 0;
midx.c +3 -0
index 5f0dd386b02..9c26d04bfde 100644
@@ -1072,6 +1072,9 @@ static int write_midx_bitmap(char *midx_name, unsigned char *midx_hash,
 	if (flags & MIDX_WRITE_BITMAP_HASH_CACHE)
 		options |= BITMAP_OPT_HASH_CACHE;
 
+	if (flags & MIDX_WRITE_BITMAP_LOOKUP_TABLE)
+		options |= BITMAP_OPT_LOOKUP_TABLE;
+
 	prepare_midx_packing_data(&pdata, ctx);
 
 	commits = find_commits_for_midx_bitmap(&commits_nr, refs_snapshot, ctx);
midx.h +1 -0
index 22e8e53288e..5578cd7b835 100644
@@ -47,6 +47,7 @@ struct multi_pack_index {
 #define MIDX_WRITE_REV_INDEX (1 << 1)
 #define MIDX_WRITE_BITMAP (1 << 2)
 #define MIDX_WRITE_BITMAP_HASH_CACHE (1 << 3)
+#define MIDX_WRITE_BITMAP_LOOKUP_TABLE (1 << 4)
 
 const unsigned char *get_midx_checksum(struct multi_pack_index *m);
 void get_midx_filename(struct strbuf *out, const char *object_dir);
t/t5310-pack-bitmaps.sh +410 -382

807 lines not on this page · raw message

t/t5311-pack-bitmaps-shallow.sh +35 -18

58 lines not on this page · raw message

t/t5326-multi-pack-bitmaps.sh +240 -181

513 lines not on this page · raw message

t/t5327-multi-pack-bitmaps-rev.sh +9 -0
index d30ba632c87..d01c61c0c7e 100755
@@ -20,4 +20,13 @@ export GIT_TEST_MIDX_READ_RIDX
 midx_bitmap_core rev
 midx_bitmap_partial_tests rev
 
+test_expect_success 'reinitialize the repository with lookup table enabled' '
+    rm -fr * .git &&
+    git init &&
+    git config pack.writeBitmapLookupTable true
+'
+
+midx_bitmap_core rev
+midx_bitmap_partial_tests rev
+
 test_done
Signature · 1 line
gitgitgadget
#110 lore raw

PATCH v4 6/6 bitmap-lookup-table: add performance tests for lookup table

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

Add performance tests to verify the performance of lookup table with pack.writeReverseIndex enabled. This is to check the performance when the above configuration is set.

Lookup table makes Git run faster in most of the cases. Below is the result of t/perf/p5310-pack-bitmaps.sh.perf/p5326-multi-pack-bitmaps.sh gives similar result. The repository used in the test is linux kernel.

Test this tree
---------------------------------------------------------------------------

5310.4: repack to disk (lookup=false)                   296.55(256.53+14.52)
5310.5: simulated clone                                 15.64(8.88+1.39)
5310.6: simulated fetch                                 1.65(2.75+0.20)
5310.7: pack to file (bitmap)                           48.71(30.20+7.58)
5310.8: rev-list (commits)                              0.61(0.41+0.08)
5310.9: rev-list (objects)                              4.38(4.26+0.09)
5310.10: rev-list with tag negated via --not            0.07(0.02+0.04)
         --all (objects)
5310.11: rev-list with negative tag (objects)           0.05(0.01+0.03)
5310.12: rev-list count with blob:none                  0.08(0.03+0.04)
5310.13: rev-list count with blob:limit=1k              7.29(6.92+0.30)
5310.14: rev-list count with tree:0                     0.08(0.03+0.04)
5310.15: simulated partial clone                        9.45(8.12+0.41)
5310.17: clone (partial bitmap)                         21.00(15.04+2.39)
5310.18: pack to file (partial bitmap)                  47.98(38.13+5.23)
5310.19: rev-list with tree filter (partial bitmap)     0.70(0.07+0.20)
5310.22: repack to disk (lookup=true)                   255.92(188.13+20.47)
5310.23: simulated clone                                13.78(8.84+1.09)
5310.24: simulated fetch                                0.52(0.63+0.14)
5310.25: pack to file (bitmap)                          44.34(28.94+6.84)
5310.26: rev-list (commits)                             0.48(0.31+0.06)
5310.27: rev-list (objects)                             4.02(3.93+0.07)
5310.28: rev-list with tag negated via --not            0.04(0.00+0.03)
         --all (objects)
5310.29: rev-list with negative tag (objects)           0.04(0.00+0.03)
5310.30: rev-list count with blob:none                  0.04(0.01+0.03)
5310.31: rev-list count with blob:limit=1k              6.48(6.23+0.22)
5310.32: rev-list count with tree:0                     0.04(0.01+0.03)
5310.33: simulated partial clone                        8.30(7.21+0.36)
5310.35: clone (partial bitmap)                         20.34(15.00+2.41)
5310.36: pack to file (partial bitmap)                  46.45(38.05+5.20)
5310.37: rev-list with tree filter (partial bitmap)     0.61(0.06+0.20)

Test 4-15 are tested without using lookup table. Same tests are repeated in 16-30 (using lookup table).

---
 t/perf/p5310-pack-bitmaps.sh       | 65 +++++++++++---------
 t/perf/p5326-multi-pack-bitmaps.sh | 95 +++++++++++++++++-------------
 2 files changed, 91 insertions(+), 69 deletions(-)
t/perf/p5310-pack-bitmaps.sh +38 -27
index 6e8abcd5b21..adc753b6177 100755
@@ -17,39 +17,50 @@ test_expect_success 'setup bitmap config' '
 	git config pack.writeReverseIndex true
 '
 
-# we need to create the tag up front such that it is covered by the repack and
-# thus by generated bitmaps.
-test_expect_success 'create tags' '
-	git tag --message="tag pointing to HEAD" perf-tag HEAD
-'
+test_bitmap () {
+	local enabled="$1"
 
-test_perf 'repack to disk' '
-	git repack -ad
-'
+	# we need to create the tag up front such that it is covered by the repack and
+	# thus by generated bitmaps.
+	test_expect_success 'create tags' '
+		git tag --message="tag pointing to HEAD" perf-tag HEAD
+	'
 
-test_full_bitmap
+	test_expect_success "use lookup table: $enabled" '
+		git config pack.writeBitmapLookupTable '"$enabled"'
+	'
 
-test_expect_success 'create partial bitmap state' '
-	# pick a commit to represent the repo tip in the past
-	cutoff=$(git rev-list HEAD~100 -1) &&
-	orig_tip=$(git rev-parse HEAD) &&
+	test_perf "repack to disk (lookup=$enabled)" '
+		git repack -ad
+	'
 
-	# now kill off all of the refs and pretend we had
-	# just the one tip
-	rm -rf .git/logs .git/refs/* .git/packed-refs &&
-	git update-ref HEAD $cutoff &&
+	test_full_bitmap
 
-	# and then repack, which will leave us with a nice
-	# big bitmap pack of the "old" history, and all of
-	# the new history will be loose, as if it had been pushed
-	# up incrementally and exploded via unpack-objects
-	git repack -Ad &&
+	test_expect_success "create partial bitmap state (lookup=$enabled)" '
+		# pick a commit to represent the repo tip in the past
+		cutoff=$(git rev-list HEAD~100 -1) &&
+		orig_tip=$(git rev-parse HEAD) &&
 
-	# and now restore our original tip, as if the pushes
-	# had happened
-	git update-ref HEAD $orig_tip
-'
+		# now kill off all of the refs and pretend we had
+		# just the one tip
+		rm -rf .git/logs .git/refs/* .git/packed-refs &&
+		git update-ref HEAD $cutoff &&
+
+		# and then repack, which will leave us with a nice
+		# big bitmap pack of the "old" history, and all of
+		# the new history will be loose, as if it had been pushed
+		# up incrementally and exploded via unpack-objects
+		git repack -Ad &&
+
+		# and now restore our original tip, as if the pushes
+		# had happened
+		git update-ref HEAD $orig_tip
+	'
+
+	test_partial_bitmap
+}
 
-test_partial_bitmap
+test_bitmap false
+test_bitmap true
 
 test_done
t/perf/p5326-multi-pack-bitmaps.sh +53 -42

100 lines not on this page · raw message

Signature · 1 line
gitgitgadget
#111 lore raw

v5 cover bitmap: integrate a lookup table extension to the bitmap format

In reply to Abhradeep Chakraborty via GitGitGadget

When parsing the .bitmap file, git loads all the bitmaps one by one even if some of the bitmaps are not necessary. We can remove this overhead by loading only the necessary bitmaps. A look up table extension can solve this issue.

Changes since v4:

* There was a CI failing test for linux-sha256 in the previous version. Fixed now.

Changes since v3:

* The common code from both lookup_table_get_triplet() and bsearch_triplet_by_pos are moved to lookup_table_get_triplet_by_pointer function
* parameter names of triplet_cmp function is changes (as suggested by Martin)
* xor_items array is now work as reusable static buffer.
* I moved the filling commit_positions array part (from pack-bitmap-write.c) to bitmap_writer_finish function. Because we had to iterate two times for commit positions - one in write_selected_commits_v1 and another in write_lookup_table function. Hope this is acceptable :)
* changes in performance tests (as suggested by Taylor)

Changes since v2:

* Log messages related issues are fixed.
* pack.writeBitmapLookupTable is now by default disabled.
* Documentations are improved.
* xor_row is used instead of xor_pos in triplets.
* In pack-bitmap-write.c, off_t * is used for offsets array (Instead of uint64_t *).
* struct bitmap_lookup_table_triplet is introduced and functions Like triplet_get_offset() and triplet_get_xor_pos() are removed.
* table_size is getting subtracted from index_end irrespective of the value of GIT_TEST_READ_COMMIT_TABLE.
* xor stack filling loop will stop iterating if a xor bitmap is already stored/parsed.
* The stack will now store bitmap_lookup_table_xor_item items Of plain xor_row.
* bitmap related test files are reformatted to allow repeating of tests with bitmap extension enabled.
* comments are added.

Changes since v1:

This is the second version which addressed all (I think) the reviews. Please notify me if some reviews are not addressed :)

* The table size is decreased and the format has also changed. It now contains nr_entries triplets of size 4+8+4 bytes. Each triplet contains the following things - (1) 4 byte commit position (in the pack-index or midx) (2) 8 byte offset and (3) 4 byte xor triplet (i.e. with whose bitmap the current triplet's bitmap has to xor) position.
* Performance tests are splitted into two commits. First contains the actual performance tests and second enables the pack.writeReverseIndex (as suggested by Taylor).
* st_*() functions are used.
* commit order is changed according to Derrick's suggestion.
* Iterative approach is used instead of recursive approach to parse xor bitmaps. (As suggested by Derrick).
* Some minor bug fixes of previous version.

Initial version:

The proposed table has:

* a list of nr_entries object ids. These objects are commits that has bitmaps. Ids are stored in lexicographic order (for better searching).
* a list of <offset, xor-offset> pairs (4-byte integers, network-byte order). The i'th pair denotes the offset and xor-offset(respectively) of the bitmap of i'th commit in the previous list. These two informations are necessary because only in this way bitmaps can be found without parsing all the bitmap.
* a 4-byte integer for table specific flags (none exists currently).

Whenever git want to parse the bitmap for a specific commit, it will first refer to the table and will look for the offset and xor-offset for that commit. Git will then try to parse the bitmap located at the offset position. The xor-offset can be used to find the xor-bitmap for the bitmap(if any).

Abhradeep Chakraborty (6):
Documentation/technical: describe bitmap lookup table extension pack-bitmap-write.c: write lookup table extension
pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
pack-bitmap: prepare to read lookup table extension p5310-pack-bitmaps.sh: enable pack.writeReverseIndex
bitmap-lookup-table: add performance tests for lookup table

 Documentation/config/pack.txt             |   7 +
 Documentation/technical/bitmap-format.txt |  39 ++
 builtin/multi-pack-index.c                |   7 +
 builtin/pack-objects.c                    |   8 +
 midx.c                                    |   3 +
 midx.h                                    |   1 +
 pack-bitmap-write.c                       | 112 ++-
 pack-bitmap.c                             | 275 +++++++-
 pack-bitmap.h                             |  14 +-
 t/perf/p5310-pack-bitmaps.sh              |  68 +-
 t/perf/p5326-multi-pack-bitmaps.sh        |  95 +--
 t/t5310-pack-bitmaps.sh                   | 786 ++++++++++++----------
 t/t5311-pack-bitmaps-shallow.sh           |  53 +-
 t/t5326-multi-pack-bitmaps.sh             | 421 +++++++-----
 t/t5327-multi-pack-bitmaps-rev.sh         |  24 +-
 15 files changed, 1254 insertions(+), 659 deletions(-)

base-commit: 39c15e485575089eb77c769f6da02f98a55905e0
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1266%2FAbhra303%2Fbitmap-commit-table-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1266/Abhra303/bitmap-commit-table-v5
Pull-Request: https://github.com/gitgitgadget/git/pull/1266

Range-diff vs v4:

1: f72bf11e6ef ! 1: 33aca8f3dc8 Documentation/technical: describe bitmap lookup table extension
@@ Commit message
even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx
- or pack, along with their offset and xor offset. This way git can
+ or pack, along with their offset and xor offset. This way Git can
load only the necessary bitmaps without loading the previous bitmaps.

Older versions of Git ignore the lookup table extension and don't

 2:  04244fadf5c = 2:  a913e6a2cb3 pack-bitmap-write.c: write lookup table extension
 3:  8bd7639e4b9 ! 3:  59b465e5a78 pack-bitmap-write: learn pack.writeBitmapLookupTable and add tests
     @@ t/t5311-pack-bitmaps-shallow.sh: test_description='check bitmap operation with s
      +}
      +
      +test_shallow_bitmaps
     -+
     ++test_shallow_bitmaps "pack.writeBitmapLookupTable"
       
       test_done
      
     @@ t/t5326-multi-pack-bitmaps.sh: bitmap_reuse_tests() {
       
      
       ## t/t5327-multi-pack-bitmaps-rev.sh ##
     -@@ t/t5327-multi-pack-bitmaps-rev.sh: export GIT_TEST_MIDX_READ_RIDX
     - midx_bitmap_core rev
     - midx_bitmap_partial_tests rev
     +@@ t/t5327-multi-pack-bitmaps-rev.sh: GIT_TEST_MIDX_READ_RIDX=0
     + export GIT_TEST_MIDX_WRITE_REV
     + export GIT_TEST_MIDX_READ_RIDX
     + 
     +-midx_bitmap_core rev
     +-midx_bitmap_partial_tests rev
     ++test_midx_bitmap_rev () {
     ++     writeLookupTable=false
     ++
     ++ 	for i in "$@"
     ++ 	do
     ++ 		case $i in
     ++ 		"pack.writeBitmapLookupTable") writeLookupTable=true;;
     ++ 		esac
     ++ 	done
     ++
     ++     test_expect_success 'setup bitmap config' '
     ++         rm -rf * .git &&
     ++         git init &&
     ++         git config pack.writeBitmapLookupTable '"$writeLookupTable"'
     ++     '
     ++
     ++     midx_bitmap_core rev
     ++     midx_bitmap_partial_tests rev
     ++ }
     ++
     ++ test_midx_bitmap_rev
     ++ test_midx_bitmap_rev "pack.writeBitmapLookupTable"
       
     -+test_expect_success 'reinitialize the repository with lookup table enabled' '
     -+    rm -fr * .git &&
     -+    git init &&
     -+    git config pack.writeBitmapLookupTable true
     -+'
     -+
     -+midx_bitmap_core rev
     -+midx_bitmap_partial_tests rev
     -+
       test_done
 4:  afc8c660ac1 ! 4:  6918f0860ad pack-bitmap: prepare to read lookup table extension
     @@ pack-bitmap.c: struct include_data {
      +	if (xor_row != 0xffffffff) {
      +		int xor_flags;
      +		khiter_t hash_pos;
     -+		uint64_t offset_xor;
      +		struct bitmap_lookup_table_xor_item *xor_item;
      +
      +		while (xor_row != 0xffffffff) {
     @@ pack-bitmap.c: struct include_data {
      +
      +		while (xor_items_nr) {
      +			xor_item = &xor_items[xor_items_nr - 1];
     -+			offset_xor = xor_item->offset;
     -+
     -+			bitmap_git->map_pos = offset_xor;
     ++			bitmap_git->map_pos = xor_item->offset;
      +			if (bitmap_git->map_size - bitmap_git->map_pos < bitmap_header_size) {
      +				error(_("corrupt ewah bitmap: truncated header for bitmap of commit \"%s\""),
      +					oid_to_hex(&xor_item->oid));
 5:  fc69489e395 = 5:  e7ef420f321 p5310-pack-bitmaps.sh: enable `pack.writeReverseIndex`
 6:  52f7d8359ee = 6:  6628001241d bitmap-lookup-table: add performance tests for lookup table
Signature · 1 line
gitgitgadget
#112 lore raw

PATCH v5 1/6 Documentation/technical: describe bitmap lookup table extension

From: Abhradeep Chakraborty <[email protected]>

When reading bitmap file, Git loads each and every bitmap one by one even if all the bitmaps are not required. A "bitmap lookup table" extension to the bitmap format can reduce the overhead of loading bitmaps which stores a list of bitmapped commit id pos (in the midx or pack, along with their offset and xor offset. This way Git can load only the necessary bitmaps without loading the previous bitmaps.

Older versions of Git ignore the lookup table extension and don't throw any kind of warning or error while parsing the bitmap file.

Add some information for the new "bitmap lookup table" extension in the bitmap-format documentation.

---
 Documentation/technical/bitmap-format.txt | 39 +++++++++++++++++++++++
 1 file changed, 39 insertions(+)
Documentation/technical/bitmap-format.txt +39 -0
index 04b3ec21785..c30dc177643 100644
@@ -67,6 +67,17 @@ MIDXs, both the bit-cache and rev-cache extensions are required.
 			pack/MIDX. The format and meaning of the name-hash is
 			described below.
 
+			** {empty}
+			BITMAP_OPT_LOOKUP_TABLE (0x10): :::
+			If present, the end of the bitmap file contains a table
+			containing a list of `N` <commit_pos, offset, xor_row>
+			triplets. The format and meaning of the table is described
+			below.
++
+NOTE: Unlike the xor_offset used to compress an individual bitmap,
+`xor_row` stores an *absolute* index into the lookup table, not a location
+relative to the current entry.
+
 		4-byte entry count (network byte order)
 
 			The total count of entries (bitmapped commits) in this bitmap index.
@@ -205,3 +216,31 @@ Note that this hashing scheme is tied to the BITMAP_OPT_HASH_CACHE flag.
 If implementations want to choose a different hashing scheme, they are
 free to do so, but MUST allocate a new header flag (because comparing
 hashes made under two different schemes would be pointless).
+
+Commit lookup table
+-------------------
+
+If the BITMAP_OPT_LOOKUP_TABLE flag is set, the last `N * (4 + 8 + 4)`
+bytes (preceding the name-hash cache and trailing hash) of the `.bitmap`
+file contains a lookup table specifying the information needed to get
+the desired bitmap from the entries without parsing previous unnecessary
+bitmaps.
+
+For a `.bitmap` containing `nr_entries` reachability bitmaps, the table
+contains a list of `nr_entries` <commit_pos, offset, xor_row> triplets
+(sorted in the ascending order of `commit_pos`). The content of i'th
+triplet is -
+
+	* {empty}
+	commit_pos (4 byte integer, network byte order): ::
+	It stores the object position of a commit (in the midx or pack
+	index).
+
+	* {empty}
+	offset (8 byte integer, network byte order): ::
+	The offset from which that commit's bitmap can be read.
+
+	* {empty}
+	xor_row (4 byte integer, network byte order): ::
+	The position of the triplet whose bitmap is used to compress
+	this one, or `0xffffffff` if no such bitmap exists.
Signature · 1 line
gitgitgadget
#113 lore raw

PATCH v5 2/6 pack-bitmap-write.c: write lookup table extension

In reply to Abhradeep Chakraborty via GitGitGadget

From: Abhradeep Chakraborty <[email protected]>

The bitmap lookup table extension was documented by an earlier change, but Git does not yet know how to write that extension.

Teach Git to write bitmap lookup table extension. The table contains the list of N <commit_pos, offset, xor_row>` triplets. These triplets are sorted according to their commit pos (ascending order). The meaning of each data in the i'th triplet is given below:

- commit_pos stores commit position (in the pack-index or midx). It is a 4 byte network byte order unsigned integer.

- offset is the position (in the bitmap file) from which that commit's bitmap can be read.

- xor_row is the position of the triplet in the lookup table whose bitmap is used to compress this bitmap, or 0xffffffff if no such bitmap exists.

---
 pack-bitmap-write.c | 112 ++++++++++++++++++++++++++++++++++++++++----
 pack-bitmap.h       |   5 +-
 2 files changed, 107 insertions(+), 10 deletions(-)
pack-bitmap-write.c +104 -8
index c43375bd344..9843790cb60 100644
@@ -650,20 +650,19 @@ static const struct object_id *oid_access(size_t pos, const void *table)
 
 static void write_selected_commits_v1(struct hashfile *f,
 				      struct pack_idx_entry **index,
-				      uint32_t index_nr)
+				      uint32_t index_nr,
+				      off_t *offsets,
+				      uint32_t *commit_positions)
 {
 	int i;
 
 	for (i = 0; i < writer.selected_nr; ++i) {
 		struct bitmapped_commit *stored = &writer.selected[i];
 
-		int commit_pos =
-			oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
+		if (offsets)
+			offsets[i] = hashfile_total(f);
 
-		if (commit_pos < 0)
-			BUG("trying to write commit not in index");
-
-		hashwrite_be32(f, commit_pos);
+		hashwrite_be32(f, commit_positions[i]);
 		hashwrite_u8(f, stored->xor_offset);
 		hashwrite_u8(f, stored->flags);
 
@@ -671,6 +670,81 @@ static void write_selected_commits_v1(struct hashfile *f,
 	}
 }
 
+static int table_cmp(const void *_va, const void *_vb, void *_data)
+{
+	uint32_t *commit_positions = _data;
+	uint32_t a = commit_positions[*(uint32_t *)_va];
+	uint32_t b = commit_positions[*(uint32_t *)_vb];
+
+	if (a > b)
+		return 1;
+	else if (a < b)
+		return -1;
+
+	return 0;
+}
+
+static void write_lookup_table(struct hashfile *f,
+			       struct pack_idx_entry **index,
+			       uint32_t index_nr,
+			       off_t *offsets,
+			       uint32_t *commit_positions)
+{
+	uint32_t i;
+	uint32_t *table, *table_inv;
+
+	ALLOC_ARRAY(table, writer.selected_nr);
+	ALLOC_ARRAY(table_inv, writer.selected_nr);
+
+	for (i = 0; i < writer.selected_nr; i++)
+		table[i] = i;
+
+	/*
+	 * At the end of this sort table[j] = i means that the i'th
+	 * bitmap corresponds to j'th bitmapped commit (among the selected
+	 * commits) in lex order of OIDs.
+	 */
+	QSORT_S(table, writer.selected_nr, table_cmp, commit_positions);
+
+	/* table_inv helps us discover that relationship (i'th bitmap
+	 * to j'th commit by j = table_inv[i])
+	 */
+	for (i = 0; i < writer.selected_nr; i++)
+		table_inv[table[i]] = i;
+
+	trace2_region_enter("pack-bitmap-write", "writing_lookup_table", the_repository);
+	for (i = 0; i < writer.selected_nr; i++) {
+		struct bitmapped_commit *selected = &writer.selected[table[i]];
+		uint32_t xor_offset = selected->xor_offset;
+		uint32_t xor_row;
+
+		if (xor_offset) {
+			/*
+			 * xor_index stores the index (in the bitmap entries)
+			 * of the corresponding xor bitmap. But we need to convert
+			 * this index into lookup table's index. So, table_inv[xor_index]
+			 * gives us the index position w.r.t. the lookup table.
+			 *
+			 * If "k = table[i] - xor_offset" then the xor base is the k'th
+			 * bitmap. `table_inv[k]` gives us the position of that bitmap
+			 * in the lookup table.
+			 */
+			uint32_t xor_index = table[i] - xor_offset;
+			xor_row = table_inv[xor_index];
+		} else {
+			xor_row = 0xffffffff;
+		}
+
+		hashwrite_be32(f, commit_positions[table[i]]);
+		hashwrite_be64(f, (uint64_t)offsets[table[i]]);
+		hashwrite_be32(f, xor_row);
+	}
+	trace2_region_leave("pack-bitmap-write", "writing_lookup_table", the_repository);
+
+	free(table);
+	free(table_inv);
+}
+
 static void write_hash_cache(struct hashfile *f,
 			     struct pack_idx_entry **index,
 			     uint32_t index_nr)
@@ -695,8 +769,10 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 {
 	static uint16_t default_version = 1;
 	static uint16_t flags = BITMAP_OPT_FULL_DAG;
+	off_t *offsets = NULL;
 	struct strbuf tmp_file = STRBUF_INIT;
 	struct hashfile *f;
+	uint32_t *commit_positions = NULL;
 
 	struct bitmap_disk_header header;
 
@@ -715,7 +791,25 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 	dump_bitmap(f, writer.trees);
 	dump_bitmap(f, writer.blobs);
 	dump_bitmap(f, writer.tags);
-	write_selected_commits_v1(f, index, index_nr);
+
+	ALLOC_ARRAY(commit_positions, writer.selected_nr);
+	for (uint32_t i = 0; i < writer.selected_nr; ++i) {
+		struct bitmapped_commit *stored = &writer.selected[i];
+		int commit_pos = oid_pos(&stored->commit->object.oid, index, index_nr, oid_access);
+
+		if (commit_pos < 0)
+			BUG(_("trying to write commit not in index"));
+
+		commit_positions[i] = commit_pos;
+	}
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		CALLOC_ARRAY(offsets, index_nr);
+
+	write_selected_commits_v1(f, index, index_nr, offsets, commit_positions);
+
+	if (options & BITMAP_OPT_LOOKUP_TABLE)
+		write_lookup_table(f, index, index_nr, offsets, commit_positions);
 
 	if (options & BITMAP_OPT_HASH_CACHE)
 		write_hash_cache(f, index, index_nr);
@@ -730,4 +824,6 @@ void bitmap_writer_finish(struct pack_idx_entry **index,
 		die_errno("unable to rename temporary bitmap file to '%s'", filename);
 
 	strbuf_release(&tmp_file);
+	free(offsets);
+	free(commit_positions);
 }
pack-bitmap.h +3 -2
index 3d3ddd77345..67a9d0fc303 100644
@@ -24,8 +24,9 @@ struct bitmap_disk_header {
 #define NEEDS_BITMAP (1u<<22)
 
 enum pack_bitmap_opts {
-	BITMAP_OPT_FULL_DAG = 1,
-	BITMAP_OPT_HASH_CACHE = 4,
+	BITMAP_OPT_FULL_DAG = 0x1,
+	BITMAP_OPT_HASH_CACHE = 0x4,
+	BITMAP_OPT_LOOKUP_TABLE = 0x10,
 };
 
 enum pack_bitmap_flags {
Signature · 1 line
gitgitgadget