git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH 2/4] commit-graph: write a Bloom filter containing changed paths for each commit

From
SZEDER Gábor <szeder.dev@gmail.com>
Date
Oct 9, 2018, 19:34 UTC
Message-ID
<20181009193445.21908-3-szeder.dev@gmail.com>
In-Reply-To
<20181009193445.21908-1-szeder.dev@gmail.com>

You can create a Bloom filter containing changed paths for each commit in the history by running:

  $ GIT_USE_POC_BLOOM_FILTER=$((8*1024*1024*8)) git commit-graph write

where the value of $GIT_USE_POC_BLOOM_FILTER must specify the number of bits used in the Bloom filter's bitmap.

Writing the Bloom filter is tied into the 'git commit-graph' command, mainly because that's where it might end up anyway, if it turns out to be useful, but for now it's written to a different file ('object/info/bloom'). No incremental updates yet, the Bloom filter is regenerated from scratch each time.

There is one single, big Bloom filter for the whole history (mainly because that was the simplest way to get this PoC experiment up and running). The Bloom filter stores tuples of (path, parent-oid, commit-oid) using the hash function:

  XOR(SHA1(path), XOR(parent-oid, commit-oid))

The resulting 20 bytes are turned into 5 unsigned 32 bit ints, which then specify the positions of the bits to set or check in the Bloom filter's bitmap (modulo the bitmap's size).

The parent oid is taken into account, because during revision walking the diff is checked in rev_compare_tree(), which compares one commit to _one_ of its parents, and in case of merge commits there are multiple rev_compare_tree() calls with the same commit but with different parent parameters.

Combining hashes with XOR is, in general, frowned upon, because of its intrinsic properties:

  XOR(A, A) = 0
  XOR(A, B) = XOR(B, A)

In this case it should be fine, because all of XOR's operands are cryptographic hashes, so we can safely assume that they'll never be the same.

Add each leading directory of the changed file, i.e. for 'dir/subdir/file' add 'dir' and 'dir/subdir' as well, so the Bloom filter could be used to speed up commands like 'git log dir/subdir', too.

Creating the Bloom filter is sloooow. Running it on git.git takes about 23s on my hardware, while

  git log --format='%H%n%P' --name-only --all >/dev/null
gathers all the information necessary for that in about 5.3s.

About 30% of the runtime is wasted by naively hashing and rehashing the same paths over and over again. A hash function faster than SHA1 could help with that; I just haven't yet bothered with spicing up memhash() and friends to produce 5 ints, and neither wanted to introduce another hash function with wideer output just yet. Or perhaps our hashmap mapping paths of files to their SHAs and the SHAs of their leading directories...

That's not the only factor though.  After ripping out all the loops
from add_changes_to_bloom_filter() there are no repeated SHA1(path)
calculations and no writes to the Bloom filter at all, i.e. all what
remains is revision walking and diffing, yet it still takes about 16s,
i.e. aroung 3 times more than the above mentioned 'git log' command.
I guess some other fields in 'struct rev_info' or 'struct
diff_options' need to be set, but both of those are huge, and I
haven't yet spotted which ones.
---
 commit-graph.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 116 insertions(+)
diff --git a/commit-graph.c b/commit-graph.c
index a1454c52a6..f415d3b41f 100644
--- a/commit-graph.c
+++ b/commit-graph.c
@@ -14,6 +14,9 @@
 #include "object-store.h"
 #include "alloc.h"
 #include "progress.h"
+#include "bloom-filter.h"
+#include "diff.h"
+#include "diffcore.h"
 
 #define GRAPH_SIGNATURE 0x43475048 /* "CGPH" */
 #define GRAPH_CHUNKID_OIDFANOUT 0x4f494446 /* "OIDF" */
@@ -709,6 +712,117 @@ static int add_ref_to_list(const char *refname,
 	return 0;
 }
 
+static void add_changes_to_bloom_filter(struct bloom_filter *bf,
+					struct commit *parent,
+					struct commit *commit,
+					struct diff_options *diffopt)
+{
+	unsigned char p_c_hash[GIT_MAX_RAWSZ];
+	int i;
+
+	hashxor(parent->object.oid.hash, commit->object.oid.hash, p_c_hash);
+
+	diff_tree_oid(&parent->object.oid, &commit->object.oid, "", diffopt);
+	diffcore_std(diffopt);
+
+	for (i = 0; i < diff_queued_diff.nr; i++) {
+		const char *path = diff_queued_diff.queue[i]->two->path;
+		const char *p = path;
+
+		/*
+		 * Add each leading directory of the changed file, i.e. for
+		 * 'dir/subdir/file' add 'dir' and 'dir/subdir' as well, so
+		 * the Bloom filter could be used to speed up commands like
+		 * 'git log dir/subdir', too.
+		 *
+		 * Note that directories are added without the trailing '/'.
+		 */
+		do {
+			git_hash_ctx ctx;
+			unsigned char name_hash[GIT_MAX_RAWSZ];
+			unsigned char hash[GIT_MAX_RAWSZ];
+
+			p = strchrnul(p + 1, '/');
+
+			/*
+			 * Beware all the wasted CPU cycles!
+			 *
+			 * Most paths change (a lot) more than once in the
+			 * history of a repository, so this hashes the same
+			 * paths over and over again, accounting for almost
+			 * 40% of the runtime.
+			 */
+			the_hash_algo->init_fn(&ctx);
+			the_hash_algo->update_fn(&ctx, path, p - path);
+			the_hash_algo->final_fn(name_hash, &ctx);
+
+			hashxor(name_hash, p_c_hash, hash);
+			bloom_filter_add_hash(bf, hash);
+		} while (*p);
+
+		diff_free_filepair(diff_queued_diff.queue[i]);
+	}
+
+	free(diff_queued_diff.queue);
+	DIFF_QUEUE_CLEAR(&diff_queued_diff);
+}
+
+static void fill_bloom_filter(struct bloom_filter *bf,
+				    struct progress *progress)
+{
+	struct rev_info revs;
+	const char *revs_argv[] = {NULL, "--all", NULL};
+	struct commit *commit;
+	int i = 0;
+
+	/* We (re-)create the bloom filter from scratch every time for now. */
+	init_revisions(&revs, NULL);
+	revs.diffopt.flags.recursive = 1;
+	setup_revisions(2, revs_argv, &revs, NULL);
+
+	if (prepare_revision_walk(&revs))
+		die("revision walk setup failed while preparing bloom filter");
+
+	while ((commit = get_revision(&revs))) {
+		struct commit_list *parent;
+
+		for (parent = commit->parents; parent; parent = parent->next)
+			add_changes_to_bloom_filter(bf, parent->item, commit,
+						    &revs.diffopt);
+
+		display_progress(progress, ++i);
+	}
+}
+
+static void write_bloom_filter(int report_progress, int commit_nr)
+{
+	struct bloom_filter bf;
+	struct progress *progress = NULL;
+	const char *v = getenv("GIT_USE_POC_BLOOM_FILTER");
+	unsigned int bitsize;
+	char *end;
+
+	if (!v)
+		return;
+
+	bitsize = strtol(v, &end, 10);
+	if (*end)
+		die("GIT_USE_POC_BLOOM_FILTER must specify the number of bits in the bloom filter (multiple of 8, n < 2^32)");
+
+	bloom_filter_init(&bf, bitsize);
+
+	if (report_progress)
+		progress = start_progress(_("Computing bloom filter"),
+					  commit_nr);
+
+	fill_bloom_filter(&bf, progress);
+
+	bloom_filter_write(&bf);
+	bloom_filter_free(&bf);
+
+	stop_progress(&progress);
+}
+
 void write_commit_graph_reachable(const char *obj_dir, int append,
 				  int report_progress)
 {
@@ -916,6 +1030,8 @@ void write_commit_graph(const char *obj_dir,
 	finalize_hashfile(f, NULL, CSUM_HASH_IN_STREAM | CSUM_FSYNC);
 	commit_lock_file(&lk);
 
+	write_bloom_filter(report_progress, commits.nr);
+
 	free(graph_name);
 	free(commits.list);
 	free(oids.list);
-- 
2.19.1.409.g0a0ee5eb6b
Previous: SZEDER GáborNext: Jeff King
Message 45 of 78 in “We should add a "git gc --auto" after "git clone" due to commit graph”
  1. Ævar Arnfjörð BjarmasonOct 3, 2018
  2. SZEDER GáborOct 3, 2018
  3. Derrick StoleeOct 3, 2018
  4. Ævar Arnfjörð BjarmasonOct 3, 2018
  5. Ævar Arnfjörð BjarmasonOct 3, 2018
  6. SZEDER GáborOct 3, 2018
  7. Ævar Arnfjörð BjarmasonOct 3, 2018
  8. SZEDER GáborOct 3, 2018
  9. Ævar Arnfjörð BjarmasonOct 3, 2018
  10. SZEDER GáborOct 3, 2018
  11. Junio C HamanoOct 5, 2018
  12. SZEDER GáborOct 10, 2018
  13. Ævar Arnfjörð BjarmasonOct 10, 2018
  14. Stefan BellerOct 3, 2018
  15. Jeff KingOct 3, 2018
  16. Ævar Arnfjörð BjarmasonOct 3, 2018
  17. Stefan BellerOct 3, 2018
  18. Ævar Arnfjörð BjarmasonOct 3, 2018
  19. Jeff KingOct 3, 2018
  20. Derrick StoleeOct 3, 2018
  21. Jeff KingOct 3, 2018
  22. SZEDER GáborOct 8, 2018
  23. Derrick StoleeOct 8, 2018
  24. SZEDER GáborOct 8, 2018
  25. Derrick StoleeOct 8, 2018
  26. Jeff KingOct 9, 2018
  27. Bloom Filters (was Re: We should add a "git gc --auto" after "git clone" due to commit graph)Derrick Stolee, Oct 9, 2018
  28. Ævar Arnfjörð BjarmasonOct 9, 2018
  29. Jeff KingOct 9, 2018
  30. Derrick StoleeOct 9, 2018
  31. Jeff KingOct 9, 2018
  32. Jeff KingOct 9, 2018
  33. 1/3 initial tree-bitmap proof of conceptJeff King, Oct 9, 2018
  34. 2/3 test-tree-bitmap: add "dump" modeJeff King, Oct 9, 2018
  35. Junio C HamanoOct 10, 2018
  36. Jeff KingOct 11, 2018
  37. 3/3 test-tree-bitmap: replace ewah with custom rle encodingJeff King, Oct 9, 2018
  38. Junio C HamanoOct 10, 2018
  39. Jeff KingOct 11, 2018
  40. Derrick StoleeOct 11, 2018
  41. Jeff KingOct 11, 2018
  42. SZEDER GáborOct 9, 2018
  43. 0/4 Bloom filter experimentSZEDER Gábor, Oct 9, 2018
  44. 1/4 Add a (very) barebones Bloom filter implementationSZEDER Gábor, Oct 9, 2018
  45. 2/4 commit-graph: write a Bloom filter containing changed paths for each commitSZEDER Gábor, Oct 9, 2018
  46. Jeff KingOct 9, 2018
  47. SZEDER GáborOct 9, 2018
  48. 3/4 revision.c: use the Bloom filter to speed up path-limited revision walksSZEDER Gábor, Oct 9, 2018
  49. 4/4 revision.c: add GIT_TRACE_BLOOM_FILTER for a bit of statisticsSZEDER Gábor, Oct 9, 2018
  50. Derrick StoleeOct 9, 2018
  51. 0/2 Per-commit filter proof of conceptJonathan Tan, Oct 11, 2018
  52. 1/2 One filter per commitJonathan Tan, Oct 11, 2018
  53. Derrick StoleeOct 11, 2018
  54. Per-commit and per-parent filters for 2 parentsJonathan Tan, Oct 11, 2018
  55. 2/2 Only make bloom filter for first parentJonathan Tan, Oct 11, 2018
  56. Ævar Arnfjörð BjarmasonOct 11, 2018
  57. Derrick StoleeOct 15, 2018
  58. Junio C HamanoOct 16, 2018
  59. Derrick StoleeOct 16, 2018
  60. Ævar Arnfjörð BjarmasonOct 16, 2018
  61. Derrick StoleeOct 16, 2018
  62. Junio C HamanoOct 18, 2018
  63. Jonathan TanOct 16, 2018
  64. Junio C HamanoOct 8, 2018
  65. Duy NguyenOct 3, 2018
  66. Duy NguyenOct 3, 2018
  67. We should add a "git gc --auto" after "git clone" due to commit graphÆvar Arnfjörð Bjarmason, Oct 4, 2018
  68. Derrick StoleeOct 5, 2018
  69. Ævar Arnfjörð BjarmasonOct 5, 2018
  70. Derrick StoleeOct 5, 2018
  71. Ævar Arnfjörð BjarmasonOct 5, 2018
  72. Jeff KingOct 5, 2018
  73. Derrick StoleeOct 5, 2018
  74. Jeff KingOct 5, 2018
  75. Derrick StoleeOct 5, 2018
  76. Jeff KingOct 5, 2018
  77. Ævar Arnfjörð BjarmasonOct 5, 2018
  78. Jeff KingOct 5, 2018

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.