{"thread":{"id":"53579","subject":"[PoC PATCH 00/34] An alternative modified path Bloom filters implementation","startedAt":"2020-05-29T08:51:00Z","lastAt":"2020-06-29T20:55:32Z","messageCount":43,"participants":["SZEDER Gábor","Derrick Stolee","Johannes Schindelin","Taylor Blau","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":34},"messages":[{"id":"398841","messageId":"20200529085038.26008-1-szeder.dev@gmail.com","threadId":"53579","inReplyTo":null,"subject":"[PoC PATCH 00/34] An alternative modified path Bloom filters implementation","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:04Z","receivedAt":"2020-05-29T08:51:00Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Sigh...  but better late than never, right?\n\nI experimented quite a bit with modified path Bloom filters a year and\nmore ago, and got quite far...  but my disappointment in the\ninadequacy of all double hashing schemes, the arrival of split\ncommit-graphs, and, well, life in general has put the whole thing on\nthe back burner, and I haven't touched it for a couple of releases.\n\nNow I finally managed to take a closer look at the current changed\npaths Bloom filters implementation, and saw that it has some of the\nsame issues that I had stumbled upon and that it missed some\noptimization opportunities.  Unfortunately, fixing those issues and\nperforming those optimizations do require a thorough format change.\n\nSo here is my proof of concept version, in all its incompleteness,\nwith the following benefits:\n\n  - Better understanding of the problem it tries to optimize.\n  - Better understanding of the issues with many small Bloom filters.\n  - Better hashing scheme (though it should be better still).\n  - Orders of magnitude lower average false positive rate.\n  - Consequently, faster pathspec-limited revision walks.\n  - Faster processing of the tree-diff output and lower memory usage\n    while computing Bloom filters (from scratch...).\n  - Optional support for storing Bloom filters for all parents of\n    merge commits.\n  - Deduplicates Bloom filters.\n  - Supports multiple pathspecs right from the start.\n  - Supports some wildcards in pathspecs.\n  - Handles as many commits as the commit-graph format can.\n  - It has the right name :)  The diff machinery and all its frontends\n    report \"modified\" paths with the letter 'M', not \"changed\".\n  - More cleanups, more bugfixes.\n  - Consistent output with and without modified path Bloom filters for\n    over 80k random paths in 16 repositories, even with submodules in\n    them.  Well, at least on my machine, if nowhere else...\n\nAlas, the drawbacks are significant:\n\n  - No tests whatsoever.\n  - Computes all modified path Bloom filters from scratch when\n    writing, no matter what.\n  - Doesn't work with split commit-graphs.\n  - Basically if anything works besides 'git commit-graph write\n    --reachable' it's a miracle.\n  - Not a single test.\n  - Many BUG()s, which should rather be graceful errors...  though I\n    have to admit that at this point they are indeed bugs.\n  - Many TODOs, both in commit messages and code, some incomplete\n    commit messages, crappy subject lines, even missing signoffs.\n  - Some ridiculously long variable, function, macro and config\n    variable names.\n  - It's based on v2.25.0 (no technical reason, but that's the version\n    I used to run the baseline benchmarks the last time, which takes\n    days...)\n  - I'm pretty sure that there are more...\n  - Oh, did I mention that there are no tests?\n\n\nThe first 14 patches are preparatory fixes and cleanups:\n\n  01/34 tree-walk.c: don't match submodule entries for 'submod/anything'\n\nThis fix or something similar is necessary to have consistent output\nwith and without modified path Bloom filters for paths crossing\nsubmodule boundary.\n\n  02/34 commit-graph: fix parsing the Chunk Lookup table\n\nThe minimal (though not the best) fix for a bug which, I think, is as\nold as the commit-graph.  I don't know how to test this.\n\n  03/34 commit-graph-format.txt: all multi-byte numbers are in network byte order\n  04/34 commit-slab: add a function to deep free entries on the slab\n  05/34 diff.h: drop diff_tree_oid() & friends' return value\n  06/34 commit-graph: clean up #includes\n\nA couple of minor cleanups.\n\n  07/34 commit-graph: simplify parse_commit_graph() #1\n  08/34 commit-graph: simplify parse_commit_graph() #2\n\nThese two would be the right, though not minimal fix for the parsing\nbug above.\n\n  09/34 commit-graph: simplify write_commit_graph_file() #1\n  10/34 commit-graph: simplify write_commit_graph_file() #2\n  11/34 commit-graph: allocate the 'struct chunk_info' array dinamically\n\nI think these three cleanup patches are a better alternative of\n3be7efcafc (commit-graph: define and use MAX_NUM_CHUNKS, 2020-03-30),\nbecause...\n\n  12/34 commit-graph: unify the signatures of all write_graph_chunk_*() functions\n  13/34 commit-graph: simplify write_commit_graph_file() #3\n  14/34 commit-graph: check chunk sizes after writing\n\n... they laid the ground work for this patch.\n\n  15/34 commit-graph-format.txt: document the modified path Bloom filter chunks\n\nThis is the most important one, specifying and _justifying_ the new\nchunk formats.\n\nDo grab a cup or pint of your favourite beverage and get comfy before\nreading this one.  You have been warned.\n\n  16/34 Add a generic and minimal Bloom filter implementation\n  17/34 Import a streaming-capable Murmur3 hash function implementation\n  18/34 commit-graph: write \"empty\" Modified Path Bloom Filter Index chunk\n  19/34 commit-graph: add commit slab for modified path Bloom filters\n  20/34 commit-graph: fill the Modified Path Bloom Filter Index chunk\n\nThis shows a more efficient approach to process the tree-diff output\ninto Bloom filters.\n\n  21/34 commit-graph: load and use the Modified Path Bloom Filter Index chunk\n  22/34 commit-graph: write the Modified Path Bloom Filters chunk\n  23/34 commit-graph: load and use the Modified Path Bloom Filters chunk\n  24/34 commit-graph: check all leading directories in modified path Bloom filters\n\nThis was a good lightbulb moment.  It is essential to try to maintain\nreasonable performance in repositories where the vast majority of\nchanges are concentrated to a single directory.\n\n  25/34 commit-graph: check embedded modified path Bloom filters with a mask\n  26/34 commit-graph: deduplicate modified path Bloom filters\n  27/34 commit-graph: load modified path Bloom filters for merge commits\n  28/34 commit-graph: write Modified Path Bloom Filter Merge Index chunk\n  29/34 commit-graph: extract init and free write_commit_graph_context\n  30/34 commit-graph: move write_commit_graph_reachable below write_commit_graph\n  31/34 t7007-show: make the first test compatible with the next patch\n  32/34 PoC commit-graph: use revision walk machinery for '--reachable'\n\nOnce upon a time I thought this was the greatest idea ever, but as\ntime goes by I get more and more concerned that this is a really dumb\nidea, though don't yet know why.\n\n  33/34 commit-graph: write modified path Bloom filters in \"history order\"\n  34/34 commit-graph: use modified path Bloom filters with wildcards, if possible\n\nFinally a cherry on top.\n\n\n\nSZEDER Gábor (34):\n  tree-walk.c: don't match submodule entries for 'submod/anything'\n  commit-graph: fix parsing the Chunk Lookup table\n  commit-graph-format.txt: all multi-byte numbers are in network byte\n    order\n  commit-slab: add a function to deep free entries on the slab\n  diff.h: drop diff_tree_oid() & friends' return value\n  commit-graph: clean up #includes\n  commit-graph: simplify parse_commit_graph() #1\n  commit-graph: simplify parse_commit_graph() #2\n  commit-graph: simplify write_commit_graph_file() #1\n  commit-graph: simplify write_commit_graph_file() #2\n  commit-graph: allocate the 'struct chunk_info' array dinamically\n  commit-graph: unify the signatures of all write_graph_chunk_*()\n    functions\n  commit-graph: simplify write_commit_graph_file() #3\n  commit-graph: check chunk sizes after writing\n  commit-graph-format.txt: document the modified path Bloom filter\n    chunks\n  Add a generic and minimal Bloom filter implementation\n  Import a streaming-capable Murmur3 hash function implementation\n  commit-graph: write \"empty\" Modified Path Bloom Filter Index chunk\n  commit-graph: add commit slab for modified path Bloom filters\n  commit-graph: fill the Modified Path Bloom Filter Index chunk\n  commit-graph: load and use the Modified Path Bloom Filter Index chunk\n  commit-graph: write the Modified Path Bloom Filters chunk\n  commit-graph: load and use the Modified Path Bloom Filters chunk\n  commit-graph: check all leading directories in modified path Bloom\n    filters\n  commit-graph: check embedded modified path Bloom filters with a mask\n  commit-graph: deduplicate modified path Bloom filters\n  commit-graph: load modified path Bloom filters for merge commits\n  commit-graph: write Modified Path Bloom Filter Merge Index chunk\n  commit-graph: extract init and free write_commit_graph_context\n  commit-graph: move write_commit_graph_reachable below\n    write_commit_graph\n  t7007-show: make the first test compatible with the next patch\n  PoC commit-graph: use revision walk machinery for '--reachable'\n  commit-graph: write modified path Bloom filters in \"history order\"\n  commit-graph: use modified path Bloom filters with wildcards, if\n    possible\n\n Documentation/config/core.txt                 |   19 +\n .../technical/commit-graph-format.txt         |  127 +-\n Makefile                                      |    2 +\n bloom-filter.c                                |   91 ++\n bloom-filter.h                                |   47 +\n commit-graph.c                                | 1239 +++++++++++++++--\n commit-graph.h                                |   24 +-\n commit-slab-decl.h                            |    1 +\n commit-slab-impl.h                            |   13 +\n commit-slab.h                                 |   10 +\n compat/PMurHash.c                             |  291 ++++\n compat/PMurHash.h                             |   62 +\n diff.h                                        |   10 +-\n pathspec.c                                    |   10 +\n pathspec.h                                    |   13 +\n revision.c                                    |   27 +-\n shallow.c                                     |   14 +-\n t/t4010-diff-pathspec.sh                      |    4 +-\n t/t5318-commit-graph.sh                       |    3 +-\n t/t7007-show.sh                               |    7 +-\n tree-diff.c                                   |   21 +-\n tree-walk.c                                   |    9 +-\n 22 files changed, 1872 insertions(+), 172 deletions(-)\n create mode 100644 bloom-filter.c\n create mode 100644 bloom-filter.h\n create mode 100644 compat/PMurHash.c\n create mode 100644 compat/PMurHash.h\n\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398842","messageId":"20200529085038.26008-2-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 01/34] tree-walk.c: don't match submodule entries for 'submod/anything'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:05Z","receivedAt":"2020-05-29T08:51:03Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Submodules should be handled the same as regular directories with\nrespect to the presence of a trailing slash, i.e. commands like:\n\n  git diff rev1 rev2 -- $path\n  git rev-list HEAD -- $path\n\nshould produce the same output whether $path is 'submod' or 'submod/'.\nThis has been fixed in commit 74b4f7f277 (tree-walk.c: ignore trailing\nslash on submodule in tree_entry_interesting(), 2014-01-23).\n\nUnfortunately, that commit had the unintended side effect to handle\n'submod/anything' the same as 'submod' and 'submod/' as well, e.g.:\n\n  $ git log --oneline --name-only -- sha1collisiondetection/whatever\n  4125f78222 sha1dc: update from upstream\n  sha1collisiondetection\n  07a20f569b Makefile: fix unaligned loads in sha1dc with UBSan\n  sha1collisiondetection\n  23e37f8e9d sha1dc: update from upstream\n  sha1collisiondetection\n  86cfd61e6b sha1dc: optionally use sha1collisiondetection as a submodule\n  sha1collisiondetection\n\nFix this by rejecting submodules as partial pathnames when their\ntrailing slash is followed by anything.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n t/t4010-diff-pathspec.sh | 4 +++-\n tree-walk.c              | 9 ++++++++-\n 2 files changed, 11 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t4010-diff-pathspec.sh b/t/t4010-diff-pathspec.sh\nindex e5ca359edf..65cc703c65 100755\n--- a/t/t4010-diff-pathspec.sh\n+++ b/t/t4010-diff-pathspec.sh\n@@ -125,7 +125,9 @@ test_expect_success 'setup submodules' '\n test_expect_success 'diff-tree ignores trailing slash on submodule path' '\n \tgit diff --name-only HEAD^ HEAD submod >expect &&\n \tgit diff --name-only HEAD^ HEAD submod/ >actual &&\n-\ttest_cmp expect actual\n+\ttest_cmp expect actual &&\n+\tgit diff --name-only HEAD^ HEAD -- submod/whatever >actual &&\n+\ttest_must_be_empty actual\n '\n \n test_expect_success 'diff multiple wildcard pathspecs' '\ndiff --git a/tree-walk.c b/tree-walk.c\nindex d5a8e096a6..7fbb150312 100644\n--- a/tree-walk.c\n+++ b/tree-walk.c\n@@ -849,7 +849,14 @@ static int match_entry(const struct pathspec_item *item,\n \tif (matchlen > pathlen) {\n \t\tif (match[pathlen] != '/')\n \t\t\treturn 0;\n-\t\tif (!S_ISDIR(entry->mode) && !S_ISGITLINK(entry->mode))\n+\t\t/*\n+\t\t * Reject non-directories as partial pathnames, except\n+\t\t * when match is a submodule with a trailing slash and\n+\t\t * nothing else (to handle 'submod/' and 'submod'\n+\t\t * uniformly).\n+\t\t */\n+\t\tif (!S_ISDIR(entry->mode) &&\n+\t\t    (!S_ISGITLINK(entry->mode) || matchlen > pathlen + 1))\n \t\t\treturn 0;\n \t}\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398843","messageId":"20200529085038.26008-3-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 02/34] commit-graph: fix parsing the Chunk Lookup table","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:06Z","receivedAt":"2020-05-29T08:51:05Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The commit-graph file format specifies that the chunks may be in any\norder.  However, if the OID Lookup chunk happens to be the last one in\nthe file, then any command attempting to access the commit-graph data\nwill fail with:\n\n  fatal: invalid commit position. commit-graph is likely corrupt\n\nIn this case the error is wrong, the commit-graph file does conform to\nthe specification, but the parsing of the Chunk Lookup table is a bit\nbuggy, and leaves the field holding the number of commits in the\ncommit-graph zero-initialized.\n\nThe number of commits in the commit-graph is determined while parsing\nthe Chunk Lookup table, by dividing the size of the OID Lookup chunk\nwith the hash size.  However, the Chunk Lookup table doesn't actually\nstore the size of the chunks, but it stores their starting offset.\nConsequently, the size of a chunk can only be calculated by\nsubtracting the starting offsets of that chunk from the offset of the\nsubsequent chunk, or in case of the last chunk from the offset\nrecorded in the terminating label.  This is currenly implemented in a\nbit complicated way: as we iterate over the entries of the Chunk\nLookup table, we check the ID of each chunk and store its starting\noffset, then we check the ID of the last seen chunk and calculate its\nsize using its previously saved offset if necessary (at the moment\nit's only necessary for the OID Lookup chunk).  Alas, while parsing\nthe Chunk Lookup table we only interate through the \"real\" chunks, but\nnever look at the terminating label, thus don't even check whether\nit's necessary to calulate the size of the last chunk.  Consequently,\nif the OID Lookup chunk is the last one, then we don't calculate its\nsize and turn don't run the piece of code determining the number of\ncommits in the commit graph, leaving the field holding that number\nunchanged (i.e. zero-initialized), eventually triggering the sanity\ncheck in load_oid_from_graph().\n\nFix this by iterating through all entries in the Chunk Lookup table,\nincluding the terminating label.\n\nNote that this is the minimal fix, suitable for the maintenance track.\nA better fix would be to simplify how the chunk sizes are calculated,\nbut that is a more invasive change, less suitable for 'maint', so that\nwill be done in later patches.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex b205e65ed1..12df74c925 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -222,7 +222,7 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \tlast_chunk_id = 0;\n \tlast_chunk_offset = 8;\n \tchunk_lookup = data + 8;\n-\tfor (i = 0; i < graph->num_chunks; i++) {\n+\tfor (i = 0; i <= graph->num_chunks; i++) {\n \t\tuint32_t chunk_id;\n \t\tuint64_t chunk_offset;\n \t\tint chunk_repeated = 0;\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398844","messageId":"20200529085038.26008-4-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 03/34] commit-graph-format.txt: all multi-byte numbers are in network byte order","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:07Z","receivedAt":"2020-05-29T08:51:05Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The commit-graph format specifies that \"All 4-byte numbers are in\nnetwork order\", but the commit-graph contains 8-byte integers as well\n(file offsets in the Chunk Lookup table), and their byte order is\nunspecified.\n\nClarify that all multi-byte integers are in network byte order.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/technical/commit-graph-format.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/Documentation/technical/commit-graph-format.txt b/Documentation/technical/commit-graph-format.txt\nindex a4f17441ae..efd0f99d8b 100644\n--- a/Documentation/technical/commit-graph-format.txt\n+++ b/Documentation/technical/commit-graph-format.txt\n@@ -29,7 +29,7 @@ the body into \"chunks\" and provide a binary lookup table at the beginning\n of the body. The header includes certain values, such as number of chunks\n and hash type.\n \n-All 4-byte numbers are in network order.\n+All multi-byte numbers are in network byte order.\n \n HEADER:\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398845","messageId":"20200529085038.26008-5-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 04/34] commit-slab: add a function to deep free entries on the slab","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:08Z","receivedAt":"2020-05-29T08:51:10Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"clear_##slabname() frees only the memory allocated for a commit slab\nitself, but entries in the commit slab might own additional memory\noutside the slab that should be freed as well.  We already have (at\nleast) one such commit slab, and this patch series is about to add one\nmore.\n\nTo free all additional memory owned by entries on the commit slab the\nuser of such a slab could iterate over all commits it knows about,\npeek whether there is a valid entry associated with each commit, and\nfree the additional memory, if any.  Or it could rely on intimate\nknowledge about the internals of the commit slab implementation, and\ncould itself iterate directly through all entries in the slab, and\nfree the additional memory.  Or it could just leak the additional\nmemory...\n\nIntroduce deep_clear_##slabname() to allow releasing memory owned by\ncommit slab entries by invoking the 'void free_fn(elemtype *ptr)'\nfunction specified as parameter for each entry in the slab.\n\nUse it in get_shallow_commits() in 'shallow.c' to replace an\nopen-coded iteration over a commit slab's entries.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-slab-decl.h |  1 +\n commit-slab-impl.h | 13 +++++++++++++\n commit-slab.h      | 10 ++++++++++\n shallow.c          | 14 +++++---------\n 4 files changed, 29 insertions(+), 9 deletions(-)\n\ndiff --git a/commit-slab-decl.h b/commit-slab-decl.h\nindex adc7b46c83..286164b7e2 100644\n--- a/commit-slab-decl.h\n+++ b/commit-slab-decl.h\n@@ -32,6 +32,7 @@ struct slabname {\t\t\t\t\t\t\t\\\n void init_ ##slabname## _with_stride(struct slabname *s, unsigned stride); \\\n void init_ ##slabname(struct slabname *s);\t\t\t\t\\\n void clear_ ##slabname(struct slabname *s);\t\t\t\t\\\n+void deep_clear_ ##slabname(struct slabname *s, void (*free_fn)(elemtype *ptr)); \\\n elemtype *slabname## _at_peek(struct slabname *s, const struct commit *c, int add_if_missing); \\\n elemtype *slabname## _at(struct slabname *s, const struct commit *c);\t\\\n elemtype *slabname## _peek(struct slabname *s, const struct commit *c)\ndiff --git a/commit-slab-impl.h b/commit-slab-impl.h\nindex 5c0eb91a5d..557738df27 100644\n--- a/commit-slab-impl.h\n+++ b/commit-slab-impl.h\n@@ -38,6 +38,19 @@ scope void clear_ ##slabname(struct slabname *s)\t\t\t\\\n \tFREE_AND_NULL(s->slab);\t\t\t\t\t\t\\\n }\t\t\t\t\t\t\t\t\t\\\n \t\t\t\t\t\t\t\t\t\\\n+scope void deep_clear_ ##slabname(struct slabname *s, void (*free_fn)(elemtype *)) \\\n+{\t\t\t\t\t\t\t\t\t\\\n+\tunsigned int i;\t\t\t\t\t\t\t\\\n+\tfor (i = 0; i < s->slab_count; i++) {\t\t\t\t\\\n+\t\tunsigned int j;\t\t\t\t\t\t\\\n+\t\tif (!s->slab[i])\t\t\t\t\t\\\n+\t\t\tcontinue;\t\t\t\t\t\\\n+\t\tfor (j = 0; j < s->slab_size; j++)\t\t\t\\\n+\t\t\tfree_fn(&s->slab[i][j * s->stride]);\t\t\\\n+\t}\t\t\t\t\t\t\t\t\\\n+\tclear_ ##slabname(s);\t\t\t\t\t\t\\\n+}\t\t\t\t\t\t\t\t\t\\\n+\t\t\t\t\t\t\t\t\t\\\n scope elemtype *slabname## _at_peek(struct slabname *s,\t\t\t\\\n \t\t\t\t\t\t  const struct commit *c, \\\n \t\t\t\t\t\t  int add_if_missing)   \\\ndiff --git a/commit-slab.h b/commit-slab.h\nindex 69bf0c807c..7bd472171e 100644\n--- a/commit-slab.h\n+++ b/commit-slab.h\n@@ -42,6 +42,16 @@\n  *\n  *   Call this function before the slab falls out of scope to avoid\n  *   leaking memory.\n+ *\n+ * - void deep_clear_indegree(struct indegree *, void (*free_fn)(int*))\n+ *\n+ *   Empties the slab, similar to clear_indegree(), but in addition it\n+ *   calls the given 'free_fn' for each slab entry to release any\n+ *   additional memory that might be owned by the entry (but not the\n+ *   entry itself!).\n+ *   Note that 'free_fn' might be called even for entries for which no\n+ *   indegree_at() call has been made; in this case 'free_fn' is invoked\n+ *   with a pointer to a zero-initialized location.\n  */\n \n #define define_commit_slab(slabname, elemtype) \\\ndiff --git a/shallow.c b/shallow.c\nindex 7fd04afed1..d02ba494ee 100644\n--- a/shallow.c\n+++ b/shallow.c\n@@ -84,6 +84,10 @@ int is_repository_shallow(struct repository *r)\n  * supports a \"valid\" flag.\n  */\n define_commit_slab(commit_depth, int *);\n+void free_depth_in_slab(int **ptr)\n+{\n+\tFREE_AND_NULL(*ptr);\n+}\n struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\tint shallow_flag, int not_shallow_flag)\n {\n@@ -150,15 +154,7 @@ struct commit_list *get_shallow_commits(struct object_array *heads, int depth,\n \t\t\t}\n \t\t}\n \t}\n-\tfor (i = 0; i < depths.slab_count; i++) {\n-\t\tint j;\n-\n-\t\tif (!depths.slab[i])\n-\t\t\tcontinue;\n-\t\tfor (j = 0; j < depths.slab_size; j++)\n-\t\t\tfree(depths.slab[i][j]);\n-\t}\n-\tclear_commit_depth(&depths);\n+\tdeep_clear_commit_depth(&depths, free_depth_in_slab);\n \n \treturn result;\n }\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398846","messageId":"20200529085038.26008-6-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 05/34] diff.h: drop diff_tree_oid() & friends' return value","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:09Z","receivedAt":"2020-05-29T08:51:13Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"ll_diff_tree_oid() has only ever returned 0 [1], so it's return value\nis basically useless.  It's only caller diff_tree_oid() has only ever\nreturned the return value of ll_diff_tree_oid() as-is [2], so its\nreturn value is just as useless.  Most of diff_tree_oid()'s callers\nsimply ignore its return value, except:\n\n  - diff_root_tree_oid() is a thin wrapper around diff_tree_oid() and\n    returns with its return value, but all of diff_root_tree_oid()'s\n    callers ignore its return value.\n\n  - rev_compare_tree() and rev_same_tree_as_empty() do look at the\n    return value in a condition, but, since the return value is always\n    0, the former's < 0 condition is never fulfilled, while the\n    latter's >= 0 condition is always fulfilled.\n\nSo let's drop the return value of ll_diff_tree_oid(), diff_tree_oid()\nand diff_root_tree_oid(), and drop those conditions from\nrev_compare_tree() and rev_same_tree_as_empty() as well.\n\n[1] ll_diff_tree_oid() and its ancestors have been returning only 0\n    ever since it was introduced as diff_tree() in 9174026cfe (Add\n    \"diff-tree\" program to show which files have changed between two\n    trees., 2005-04-09).\n[2] diff_tree_oid() traces back to diff-tree.c:main() in 9174026cfe as\n    well.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n diff.h      | 10 +++++-----\n revision.c  |  9 +++------\n tree-diff.c | 21 +++++++++------------\n 3 files changed, 17 insertions(+), 23 deletions(-)\n\ndiff --git a/diff.h b/diff.h\nindex 6febe7e365..a1459de824 100644\n--- a/diff.h\n+++ b/diff.h\n@@ -426,11 +426,11 @@ struct combine_diff_path *diff_tree_paths(\n \tstruct combine_diff_path *p, const struct object_id *oid,\n \tconst struct object_id **parents_oid, int nparent,\n \tstruct strbuf *base, struct diff_options *opt);\n-int diff_tree_oid(const struct object_id *old_oid,\n-\t\t  const struct object_id *new_oid,\n-\t\t  const char *base, struct diff_options *opt);\n-int diff_root_tree_oid(const struct object_id *new_oid, const char *base,\n-\t\t       struct diff_options *opt);\n+void diff_tree_oid(const struct object_id *old_oid,\n+\t\t   const struct object_id *new_oid,\n+\t\t   const char *base, struct diff_options *opt);\n+void diff_root_tree_oid(const struct object_id *new_oid, const char *base,\n+\t\t\tstruct diff_options *opt);\n \n struct combine_diff_path {\n \tstruct combine_diff_path *next;\ndiff --git a/revision.c b/revision.c\nindex 8136929e23..9dac1262ef 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -655,15 +655,12 @@ static int rev_compare_tree(struct rev_info *revs,\n \n \ttree_difference = REV_TREE_SAME;\n \trevs->pruning.flags.has_changes = 0;\n-\tif (diff_tree_oid(&t1->object.oid, &t2->object.oid, \"\",\n-\t\t\t   &revs->pruning) < 0)\n-\t\treturn REV_TREE_DIFFERENT;\n+\tdiff_tree_oid(&t1->object.oid, &t2->object.oid, \"\", &revs->pruning);\n \treturn tree_difference;\n }\n \n static int rev_same_tree_as_empty(struct rev_info *revs, struct commit *commit)\n {\n-\tint retval;\n \tstruct tree *t1 = get_commit_tree(commit);\n \n \tif (!t1)\n@@ -671,9 +668,9 @@ static int rev_same_tree_as_empty(struct rev_info *revs, struct commit *commit)\n \n \ttree_difference = REV_TREE_SAME;\n \trevs->pruning.flags.has_changes = 0;\n-\tretval = diff_tree_oid(NULL, &t1->object.oid, \"\", &revs->pruning);\n+\tdiff_tree_oid(NULL, &t1->object.oid, \"\", &revs->pruning);\n \n-\treturn retval >= 0 && (tree_difference == REV_TREE_SAME);\n+\treturn tree_difference == REV_TREE_SAME;\n }\n \n struct treesame_state {\ndiff --git a/tree-diff.c b/tree-diff.c\nindex 33ded7f8b3..9775e4bcaa 100644\n--- a/tree-diff.c\n+++ b/tree-diff.c\n@@ -29,7 +29,7 @@ static struct combine_diff_path *ll_diff_tree_paths(\n \tstruct combine_diff_path *p, const struct object_id *oid,\n \tconst struct object_id **parents_oid, int nparent,\n \tstruct strbuf *base, struct diff_options *opt);\n-static int ll_diff_tree_oid(const struct object_id *old_oid,\n+static void ll_diff_tree_oid(const struct object_id *old_oid,\n \t\t\t    const struct object_id *new_oid,\n \t\t\t    struct strbuf *base, struct diff_options *opt);\n \n@@ -673,7 +673,7 @@ static void try_to_follow_renames(const struct object_id *old_oid,\n \tq->nr = 1;\n }\n \n-static int ll_diff_tree_oid(const struct object_id *old_oid,\n+static void ll_diff_tree_oid(const struct object_id *old_oid,\n \t\t\t    const struct object_id *new_oid,\n \t\t\t    struct strbuf *base, struct diff_options *opt)\n {\n@@ -691,29 +691,26 @@ static int ll_diff_tree_oid(const struct object_id *old_oid,\n \t}\n \n \topt->pathchange = pathchange_old;\n-\treturn 0;\n }\n \n-int diff_tree_oid(const struct object_id *old_oid,\n-\t\t  const struct object_id *new_oid,\n-\t\t  const char *base_str, struct diff_options *opt)\n+void diff_tree_oid(const struct object_id *old_oid,\n+\t\t   const struct object_id *new_oid,\n+\t\t   const char *base_str, struct diff_options *opt)\n {\n \tstruct strbuf base;\n-\tint retval;\n \n \tstrbuf_init(&base, PATH_MAX);\n \tstrbuf_addstr(&base, base_str);\n \n-\tretval = ll_diff_tree_oid(old_oid, new_oid, &base, opt);\n+\tll_diff_tree_oid(old_oid, new_oid, &base, opt);\n \tif (!*base_str && opt->flags.follow_renames && diff_might_be_rename())\n \t\ttry_to_follow_renames(old_oid, new_oid, &base, opt);\n \n \tstrbuf_release(&base);\n-\n-\treturn retval;\n }\n \n-int diff_root_tree_oid(const struct object_id *new_oid, const char *base, struct diff_options *opt)\n+void diff_root_tree_oid(const struct object_id *new_oid, const char *base,\n+\t\t\tstruct diff_options *opt)\n {\n-\treturn diff_tree_oid(NULL, new_oid, base, opt);\n+\tdiff_tree_oid(NULL, new_oid, base, opt);\n }\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398847","messageId":"20200529085038.26008-8-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 07/34] commit-graph: simplify parse_commit_graph() #1","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:11Z","receivedAt":"2020-05-29T08:51:14Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"While we iterate over all entries of the Chunk Lookup table we make\nsure that we don't attempt to read past the end of the mmap-ed\ncommit-graph file, and check in each iteration that the chunk ID and\noffset we are about to read is still within the mmap-ed memory region.\nHowever, these checks in each iteration are not really necessary,\nbecause the number of chunks in the commit-graph file is already known\nbefore this loop from the just parsed commit-graph header.\n\nSo let's check that the commit-graph file is large enough for all\nentries in the Chunk Lookup table before we start iterating over those\nentries, and drop those per-iteration checks.  While at it, take into\naccount the size of everything that is necessary to have a valid\ncommit-graph file, i.e. the size of the header, the size of the\nmandatory OID Fanout chunk, and the size of the signature in the\ntrailer as well.\n\nNote that this necessitates the change of the error message as well,\nand, consequently, have to update the 'detect incorrect chunk count'\ntest in 't5318-commit-graph.sh' as well.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c          | 16 +++++++++-------\n t/t5318-commit-graph.sh |  3 ++-\n 2 files changed, 11 insertions(+), 8 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex f5ba8a9578..69facf3bf5 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -217,6 +217,15 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \tgraph->data = graph_map;\n \tgraph->data_len = graph_size;\n \n+\tif (graph_size < GRAPH_HEADER_SIZE +\n+\t\t\t (graph->num_chunks + 1) * GRAPH_CHUNKLOOKUP_WIDTH +\n+\t\t\t GRAPH_FANOUT_SIZE + the_hash_algo->rawsz) {\n+\t\terror(_(\"commit-graph file is too small to hold %u chunks\"),\n+\t\t      graph->num_chunks);\n+\t\tfree(graph);\n+\t\treturn NULL;\n+\t}\n+\n \tlast_chunk_id = 0;\n \tlast_chunk_offset = 8;\n \tchunk_lookup = data + 8;\n@@ -225,13 +234,6 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\tuint64_t chunk_offset;\n \t\tint chunk_repeated = 0;\n \n-\t\tif (data + graph_size - chunk_lookup <\n-\t\t    GRAPH_CHUNKLOOKUP_WIDTH) {\n-\t\t\terror(_(\"commit-graph chunk lookup table entry missing; file may be incomplete\"));\n-\t\t\tfree(graph);\n-\t\t\treturn NULL;\n-\t\t}\n-\n \t\tchunk_id = get_be32(chunk_lookup + 0);\n \t\tchunk_offset = get_be64(chunk_lookup + 4);\n \ndiff --git a/t/t5318-commit-graph.sh b/t/t5318-commit-graph.sh\nindex 3f03de6018..b9a5101c09 100755\n--- a/t/t5318-commit-graph.sh\n+++ b/t/t5318-commit-graph.sh\n@@ -572,7 +572,8 @@ test_expect_success 'detect invalid checksum hash' '\n \n test_expect_success 'detect incorrect chunk count' '\n \tcorrupt_graph_and_verify $GRAPH_BYTE_CHUNK_COUNT \"\\377\" \\\n-\t\t\"chunk lookup table entry missing\" $GRAPH_CHUNK_LOOKUP_OFFSET\n+\t\t\"commit-graph file is too small to hold [0-9]* chunks\" \\\n+\t\t$GRAPH_CHUNK_LOOKUP_OFFSET\n '\n \n test_expect_success 'git fsck (checks commit-graph)' '\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398848","messageId":"20200529085038.26008-7-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 06/34] commit-graph: clean up #includes","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:10Z","receivedAt":"2020-05-29T08:51:14Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Our CodingGuidelines says that it's sufficient to include one of\n'git-compat-util.h' and 'cache.h', but both 'commit-graph.c' and\n'commit-graph.h' include both.  Let's include only 'git-compat-util.h'\nto loose a bunch of unnecessary dependencies; but include 'hash.h',\nbecause 'commit-graph.h' does require the definition of 'struct\nobject_id'.\n\n'commit-graph.h' explicitly includes 'repository.h' and\n'string-list.h', but only needs the declaration of a few structs from\nthem.  Drop these includes and forward-declare the necessary structs\ninstead.\n\n'commit-graph.c' includes 'dir.h', but doesn't actually use anything\nfrom there, so let's drop that #include as well.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 4 +---\n commit-graph.h | 7 ++++---\n 2 files changed, 5 insertions(+), 6 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 12df74c925..f5ba8a9578 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1,7 +1,5 @@\n-#include \"cache.h\"\n-#include \"config.h\"\n-#include \"dir.h\"\n #include \"git-compat-util.h\"\n+#include \"config.h\"\n #include \"lockfile.h\"\n #include \"pack.h\"\n #include \"packfile.h\"\ndiff --git a/commit-graph.h b/commit-graph.h\nindex 7f5c933fa2..847cd25cfc 100644\n--- a/commit-graph.h\n+++ b/commit-graph.h\n@@ -2,14 +2,15 @@\n #define COMMIT_GRAPH_H\n \n #include \"git-compat-util.h\"\n-#include \"repository.h\"\n-#include \"string-list.h\"\n-#include \"cache.h\"\n+#include \"hash.h\"\n \n #define GIT_TEST_COMMIT_GRAPH \"GIT_TEST_COMMIT_GRAPH\"\n #define GIT_TEST_COMMIT_GRAPH_DIE_ON_LOAD \"GIT_TEST_COMMIT_GRAPH_DIE_ON_LOAD\"\n \n struct commit;\n+struct repository;\n+struct raw_object_store;\n+struct string_list;\n \n char *get_commit_graph_filename(const char *obj_dir);\n int open_commit_graph(const char *graph_file, int *fd, struct stat *st);\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398849","messageId":"20200529085038.26008-10-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 09/34] commit-graph: simplify write_commit_graph_file() #1","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:13Z","receivedAt":"2020-05-29T08:51:17Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"In write_commit_graph_file() one block of code fills the array of\nchunk IDs, another block of code fills the array of chunk offsets,\nthen the chunk IDs and offsets are written to the Chunk Lookup table,\nand finally a third block of code writes the actual chunks.  In case\nof optional chunks like Extra Edge List and Base Graphs List there is\nalso a condition checking whether that chunk is necessary/desired, and\nthat same condition is repeated in all those three blocks of code.\nThis patch series is about to add more optional chunks, so there would\nbe even more repeated conditions.\n\nThose chunk offsets are relative to the beginning of the file, so they\ninherently depend on the size of the Chunk Lookup table, which in turn\ndepends on the number of chunks that are to be written to the\ncommit-graph file.  IOW at the time we set the first chunk's ID we\ncan't yet know its offset, because we don't yet know how many chunks\nthere are.\n\nSimplify this by initially filling an array of chunk sizes, not\noffsets, and calculate the offsets based on the chunk sizes only\nlater, while we are writing the Chunk Lookup table.  This way we can\nfill the arrays of chunk IDs and sizes in one go, eliminating one set\nof repeated conditions.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 33 +++++++++++++--------------------\n 1 file changed, 13 insertions(+), 20 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex f64b1c01a8..416f4b2468 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1351,10 +1351,11 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \tstruct hashfile *f;\n \tstruct lock_file lk = LOCK_INIT;\n \tuint32_t chunk_ids[6];\n-\tuint64_t chunk_offsets[6];\n+\tuint64_t chunk_sizes[6];\n \tconst unsigned hashsz = the_hash_algo->rawsz;\n \tstruct strbuf progress_title = STRBUF_INIT;\n \tint num_chunks = 3;\n+\tuint64_t chunk_offset;\n \tstruct object_id file_hash;\n \n \tif (ctx->split) {\n@@ -1394,35 +1395,24 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t}\n \n \tchunk_ids[0] = GRAPH_CHUNKID_OIDFANOUT;\n+\tchunk_sizes[0] = GRAPH_FANOUT_SIZE;\n \tchunk_ids[1] = GRAPH_CHUNKID_OIDLOOKUP;\n+\tchunk_sizes[1] = hashsz * ctx->commits.nr;\n \tchunk_ids[2] = GRAPH_CHUNKID_DATA;\n+\tchunk_sizes[2] = (hashsz + 16) * ctx->commits.nr;\n \tif (ctx->num_extra_edges) {\n \t\tchunk_ids[num_chunks] = GRAPH_CHUNKID_EXTRAEDGES;\n+\t\tchunk_sizes[3] = 4 * ctx->num_extra_edges;\n \t\tnum_chunks++;\n \t}\n \tif (ctx->num_commit_graphs_after > 1) {\n \t\tchunk_ids[num_chunks] = GRAPH_CHUNKID_BASE;\n+\t\tchunk_sizes[4] = hashsz * (ctx->num_commit_graphs_after - 1);\n \t\tnum_chunks++;\n \t}\n \n \tchunk_ids[num_chunks] = 0;\n-\n-\tchunk_offsets[0] = 8 + (num_chunks + 1) * GRAPH_CHUNKLOOKUP_WIDTH;\n-\tchunk_offsets[1] = chunk_offsets[0] + GRAPH_FANOUT_SIZE;\n-\tchunk_offsets[2] = chunk_offsets[1] + hashsz * ctx->commits.nr;\n-\tchunk_offsets[3] = chunk_offsets[2] + (hashsz + 16) * ctx->commits.nr;\n-\n-\tnum_chunks = 3;\n-\tif (ctx->num_extra_edges) {\n-\t\tchunk_offsets[num_chunks + 1] = chunk_offsets[num_chunks] +\n-\t\t\t\t\t\t4 * ctx->num_extra_edges;\n-\t\tnum_chunks++;\n-\t}\n-\tif (ctx->num_commit_graphs_after > 1) {\n-\t\tchunk_offsets[num_chunks + 1] = chunk_offsets[num_chunks] +\n-\t\t\t\t\t\thashsz * (ctx->num_commit_graphs_after - 1);\n-\t\tnum_chunks++;\n-\t}\n+\tchunk_sizes[num_chunks] = 0;\n \n \thashwrite_be32(f, GRAPH_SIGNATURE);\n \n@@ -1431,13 +1421,16 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \thashwrite_u8(f, num_chunks);\n \thashwrite_u8(f, ctx->num_commit_graphs_after - 1);\n \n+\tchunk_offset = 8 + (num_chunks + 1) * GRAPH_CHUNKLOOKUP_WIDTH;\n \tfor (i = 0; i <= num_chunks; i++) {\n \t\tuint32_t chunk_write[3];\n \n \t\tchunk_write[0] = htonl(chunk_ids[i]);\n-\t\tchunk_write[1] = htonl(chunk_offsets[i] >> 32);\n-\t\tchunk_write[2] = htonl(chunk_offsets[i] & 0xffffffff);\n+\t\tchunk_write[1] = htonl(chunk_offset >> 32);\n+\t\tchunk_write[2] = htonl(chunk_offset & 0xffffffff);\n \t\thashwrite(f, chunk_write, 12);\n+\n+\t\tchunk_offset += chunk_sizes[i];\n \t}\n \n \tif (ctx->report_progress) {\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398850","messageId":"20200529085038.26008-11-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 10/34] commit-graph: simplify write_commit_graph_file() #2","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:14Z","receivedAt":"2020-05-29T08:51:18Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Unify the 'chunk_ids' and 'chunk_sizes' arrays into an array of\n'struct chunk_info'.  This will allow more cleanups in the following\npatches.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 36 ++++++++++++++++++++----------------\n 1 file changed, 20 insertions(+), 16 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 416f4b2468..d95c739c10 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1344,14 +1344,18 @@ static int write_graph_chunk_base(struct hashfile *f,\n \treturn 0;\n }\n \n+struct chunk_info {\n+\tuint32_t id;\n+\tuint64_t size;\n+};\n+\n static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n {\n \tuint32_t i;\n \tint fd;\n \tstruct hashfile *f;\n \tstruct lock_file lk = LOCK_INIT;\n-\tuint32_t chunk_ids[6];\n-\tuint64_t chunk_sizes[6];\n+\tstruct chunk_info chunks[6];\n \tconst unsigned hashsz = the_hash_algo->rawsz;\n \tstruct strbuf progress_title = STRBUF_INIT;\n \tint num_chunks = 3;\n@@ -1394,25 +1398,25 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tf = hashfd(lk.tempfile->fd, lk.tempfile->filename.buf);\n \t}\n \n-\tchunk_ids[0] = GRAPH_CHUNKID_OIDFANOUT;\n-\tchunk_sizes[0] = GRAPH_FANOUT_SIZE;\n-\tchunk_ids[1] = GRAPH_CHUNKID_OIDLOOKUP;\n-\tchunk_sizes[1] = hashsz * ctx->commits.nr;\n-\tchunk_ids[2] = GRAPH_CHUNKID_DATA;\n-\tchunk_sizes[2] = (hashsz + 16) * ctx->commits.nr;\n+\tchunks[0].id = GRAPH_CHUNKID_OIDFANOUT;\n+\tchunks[0].size = GRAPH_FANOUT_SIZE;\n+\tchunks[1].id = GRAPH_CHUNKID_OIDLOOKUP;\n+\tchunks[1].size = hashsz * ctx->commits.nr;\n+\tchunks[2].id = GRAPH_CHUNKID_DATA;\n+\tchunks[2].size = (hashsz + 16) * ctx->commits.nr;\n \tif (ctx->num_extra_edges) {\n-\t\tchunk_ids[num_chunks] = GRAPH_CHUNKID_EXTRAEDGES;\n-\t\tchunk_sizes[3] = 4 * ctx->num_extra_edges;\n+\t\tchunks[num_chunks].id = GRAPH_CHUNKID_EXTRAEDGES;\n+\t\tchunks[num_chunks].size = 4 * ctx->num_extra_edges;\n \t\tnum_chunks++;\n \t}\n \tif (ctx->num_commit_graphs_after > 1) {\n-\t\tchunk_ids[num_chunks] = GRAPH_CHUNKID_BASE;\n-\t\tchunk_sizes[4] = hashsz * (ctx->num_commit_graphs_after - 1);\n+\t\tchunks[num_chunks].id = GRAPH_CHUNKID_BASE;\n+\t\tchunks[num_chunks].size = hashsz * (ctx->num_commit_graphs_after - 1);\n \t\tnum_chunks++;\n \t}\n \n-\tchunk_ids[num_chunks] = 0;\n-\tchunk_sizes[num_chunks] = 0;\n+\tchunks[num_chunks].id = 0;\n+\tchunks[num_chunks].size = 0;\n \n \thashwrite_be32(f, GRAPH_SIGNATURE);\n \n@@ -1425,12 +1429,12 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \tfor (i = 0; i <= num_chunks; i++) {\n \t\tuint32_t chunk_write[3];\n \n-\t\tchunk_write[0] = htonl(chunk_ids[i]);\n+\t\tchunk_write[0] = htonl(chunks[i].id);\n \t\tchunk_write[1] = htonl(chunk_offset >> 32);\n \t\tchunk_write[2] = htonl(chunk_offset & 0xffffffff);\n \t\thashwrite(f, chunk_write, 12);\n \n-\t\tchunk_offset += chunk_sizes[i];\n+\t\tchunk_offset += chunks[i].size;\n \t}\n \n \tif (ctx->report_progress) {\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398851","messageId":"20200529085038.26008-9-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 08/34] commit-graph: simplify parse_commit_graph() #2","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:12Z","receivedAt":"2020-05-29T08:51:19Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The Chunk Lookup table stores the chunks' starting offset in the\ncommit-graph file, not their sizes.  Consequently, the size of a chunk\ncan only be calculated by subtracting its offset from the offset of\nthe subsequent chunk (or that of the terminating label).  This is\ncurrenly implemented in a bit complicated way: as we iterate over the\nentries of the Chunk Lookup table, we check the id of each chunk and\nstore its starting offset, then we check the id of the last seen chunk\nand calculate its size using its previously saved offset.  At the\nmoment there is only one chunk for which we calculate its size, but\nthis patch series will add more, and the repeated chunk id checks are\nnot that pretty.\n\nInstead let's read ahead the offset of the next chunk on each\niteration, so we can calculate the size of each chunk right away,\nright where we store its starting offset.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 26 ++++++++++----------------\n 1 file changed, 10 insertions(+), 16 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 69facf3bf5..f64b1c01a8 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -175,8 +175,7 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \tconst unsigned char *data, *chunk_lookup;\n \tuint32_t i;\n \tstruct commit_graph *graph;\n-\tuint64_t last_chunk_offset;\n-\tuint32_t last_chunk_id;\n+\tuint64_t next_chunk_offset;\n \tuint32_t graph_signature;\n \tunsigned char graph_version, hash_version;\n \n@@ -226,16 +225,17 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\treturn NULL;\n \t}\n \n-\tlast_chunk_id = 0;\n-\tlast_chunk_offset = 8;\n \tchunk_lookup = data + 8;\n-\tfor (i = 0; i <= graph->num_chunks; i++) {\n+\tnext_chunk_offset = get_be64(chunk_lookup + 4);\n+\tfor (i = 0; i < graph->num_chunks; i++) {\n \t\tuint32_t chunk_id;\n \t\tuint64_t chunk_offset;\n \t\tint chunk_repeated = 0;\n \n \t\tchunk_id = get_be32(chunk_lookup + 0);\n-\t\tchunk_offset = get_be64(chunk_lookup + 4);\n+\t\tchunk_offset = next_chunk_offset;\n+\t\tnext_chunk_offset = get_be64(chunk_lookup + 4 +\n+\t\t\t\t\t     GRAPH_CHUNKLOOKUP_WIDTH);\n \n \t\tchunk_lookup += GRAPH_CHUNKLOOKUP_WIDTH;\n \n@@ -257,8 +257,11 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\tcase GRAPH_CHUNKID_OIDLOOKUP:\n \t\t\tif (graph->chunk_oid_lookup)\n \t\t\t\tchunk_repeated = 1;\n-\t\t\telse\n+\t\t\telse {\n \t\t\t\tgraph->chunk_oid_lookup = data + chunk_offset;\n+\t\t\t\tgraph->num_commits = (next_chunk_offset - chunk_offset)\n+\t\t\t\t\t\t     / graph->hash_len;\n+\t\t\t}\n \t\t\tbreak;\n \n \t\tcase GRAPH_CHUNKID_DATA:\n@@ -287,15 +290,6 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\t\tfree(graph);\n \t\t\treturn NULL;\n \t\t}\n-\n-\t\tif (last_chunk_id == GRAPH_CHUNKID_OIDLOOKUP)\n-\t\t{\n-\t\t\tgraph->num_commits = (chunk_offset - last_chunk_offset)\n-\t\t\t\t\t     / graph->hash_len;\n-\t\t}\n-\n-\t\tlast_chunk_id = chunk_id;\n-\t\tlast_chunk_offset = chunk_offset;\n \t}\n \n \thashcpy(graph->oid.hash, graph->data + graph->data_len - graph->hash_len);\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398852","messageId":"20200529085038.26008-12-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 11/34] commit-graph: allocate the 'struct chunk_info' array dinamically","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:15Z","receivedAt":"2020-05-29T08:51:20Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"During writing a new commit-graph file, the size of the 'struct\nchunk_info' array is hardcoded to be the maximum number of possible\nchunks plus one for the terminating label.  This array size must be\nadjusted each time when a new chunk is added, which can be easily\nforgotten.\n\nSo let's make the constant-sized 'struct chunk_info' array an\nALLOC_GROW()-able array instead.\n\nThere are a couple of cases where writing a new commit-graph file\nreturns with error; those spots were adjusted to free the dinamically\nallocated 'struct chunk_info' array.\n\nArguably the several ALLOC_GROW() call sites look unusal, because we\nusually call ALLOC_GROW() in loops, once in each iteration...\nHowever, this way anyone who adds a new chunk will notice that they\nhave to cargo-cult the ALLOC_GROW() call as well.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 74 ++++++++++++++++++++++++++++++++------------------\n 1 file changed, 47 insertions(+), 27 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex d95c739c10..c8ba17e457 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1355,12 +1355,13 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \tint fd;\n \tstruct hashfile *f;\n \tstruct lock_file lk = LOCK_INIT;\n-\tstruct chunk_info chunks[6];\n+\tstruct chunk_info *chunks = NULL;\n+\tint chunks_nr = 0, chunks_alloc = 0;\n \tconst unsigned hashsz = the_hash_algo->rawsz;\n \tstruct strbuf progress_title = STRBUF_INIT;\n-\tint num_chunks = 3;\n \tuint64_t chunk_offset;\n \tstruct object_id file_hash;\n+\tint ret = 0;\n \n \tif (ctx->split) {\n \t\tstruct strbuf tmp_file = STRBUF_INIT;\n@@ -1398,35 +1399,48 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tf = hashfd(lk.tempfile->fd, lk.tempfile->filename.buf);\n \t}\n \n-\tchunks[0].id = GRAPH_CHUNKID_OIDFANOUT;\n-\tchunks[0].size = GRAPH_FANOUT_SIZE;\n-\tchunks[1].id = GRAPH_CHUNKID_OIDLOOKUP;\n-\tchunks[1].size = hashsz * ctx->commits.nr;\n-\tchunks[2].id = GRAPH_CHUNKID_DATA;\n-\tchunks[2].size = (hashsz + 16) * ctx->commits.nr;\n+\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\tchunks[chunks_nr].id = GRAPH_CHUNKID_OIDFANOUT;\n+\tchunks[chunks_nr].size = GRAPH_FANOUT_SIZE;\n+\tchunks_nr++;\n+\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\tchunks[chunks_nr].id = GRAPH_CHUNKID_OIDLOOKUP;\n+\tchunks[chunks_nr].size = hashsz * ctx->commits.nr;\n+\tchunks_nr++;\n+\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\tchunks[chunks_nr].id = GRAPH_CHUNKID_DATA;\n+\tchunks[chunks_nr].size = (hashsz + 16) * ctx->commits.nr;\n+\tchunks_nr++;\n \tif (ctx->num_extra_edges) {\n-\t\tchunks[num_chunks].id = GRAPH_CHUNKID_EXTRAEDGES;\n-\t\tchunks[num_chunks].size = 4 * ctx->num_extra_edges;\n-\t\tnum_chunks++;\n+\t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\t\tchunks[chunks_nr].id = GRAPH_CHUNKID_EXTRAEDGES;\n+\t\tchunks[chunks_nr].size = 4 * ctx->num_extra_edges;\n+\t\tchunks_nr++;\n \t}\n \tif (ctx->num_commit_graphs_after > 1) {\n-\t\tchunks[num_chunks].id = GRAPH_CHUNKID_BASE;\n-\t\tchunks[num_chunks].size = hashsz * (ctx->num_commit_graphs_after - 1);\n-\t\tnum_chunks++;\n+\t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\t\tchunks[chunks_nr].id = GRAPH_CHUNKID_BASE;\n+\t\tchunks[chunks_nr].size = hashsz * (ctx->num_commit_graphs_after - 1);\n+\t\tchunks_nr++;\n \t}\n \n-\tchunks[num_chunks].id = 0;\n-\tchunks[num_chunks].size = 0;\n+\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\tchunks[chunks_nr].id = 0;\n+\tchunks[chunks_nr].size = 0;\n+\t/*\n+\t * Do not increase 'chunks_nr', so it still reflects the number of\n+\t * actual chunks, without the Chunk Lookup table's terminating label.\n+\t */\n \n \thashwrite_be32(f, GRAPH_SIGNATURE);\n \n \thashwrite_u8(f, GRAPH_VERSION);\n \thashwrite_u8(f, oid_version());\n-\thashwrite_u8(f, num_chunks);\n+\thashwrite_u8(f, chunks_nr);\n \thashwrite_u8(f, ctx->num_commit_graphs_after - 1);\n \n-\tchunk_offset = 8 + (num_chunks + 1) * GRAPH_CHUNKLOOKUP_WIDTH;\n-\tfor (i = 0; i <= num_chunks; i++) {\n+\tchunk_offset = 8 + (chunks_nr + 1) * GRAPH_CHUNKLOOKUP_WIDTH;\n+\tfor (i = 0; i <= chunks_nr; i++) {\n \t\tuint32_t chunk_write[3];\n \n \t\tchunk_write[0] = htonl(chunks[i].id);\n@@ -1441,11 +1455,11 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tstrbuf_addf(&progress_title,\n \t\t\t    Q_(\"Writing out commit graph in %d pass\",\n \t\t\t       \"Writing out commit graph in %d passes\",\n-\t\t\t       num_chunks),\n-\t\t\t    num_chunks);\n+\t\t\t       chunks_nr),\n+\t\t\t    chunks_nr);\n \t\tctx->progress = start_delayed_progress(\n \t\t\tprogress_title.buf,\n-\t\t\tnum_chunks * ctx->commits.nr);\n+\t\t\tchunks_nr * ctx->commits.nr);\n \t}\n \twrite_graph_chunk_fanout(f, ctx);\n \twrite_graph_chunk_oids(f, hashsz, ctx);\n@@ -1454,7 +1468,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\twrite_graph_chunk_extra_edges(f, ctx);\n \tif (ctx->num_commit_graphs_after > 1 &&\n \t    write_graph_chunk_base(f, ctx)) {\n-\t\treturn -1;\n+\t\tret = -1;\n+\t\tgoto cleanup;\n \t}\n \tstop_progress(&ctx->progress);\n \tstrbuf_release(&progress_title);\n@@ -1481,7 +1496,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \n \t\tif (!chainf) {\n \t\t\terror(_(\"unable to open commit-graph chain file\"));\n-\t\t\treturn -1;\n+\t\t\tret = -1;\n+\t\t\tgoto cleanup;\n \t\t}\n \n \t\tif (ctx->base_graph_name) {\n@@ -1493,7 +1509,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \n \t\t\t\tif (result) {\n \t\t\t\t\terror(_(\"failed to rename base commit-graph file\"));\n-\t\t\t\t\treturn -1;\n+\t\t\t\t\tret = -1;\n+\t\t\t\t\tgoto cleanup;\n \t\t\t\t}\n \t\t\t}\n \t\t} else {\n@@ -1513,13 +1530,16 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \n \t\tif (result) {\n \t\t\terror(_(\"failed to rename temporary commit-graph file\"));\n-\t\t\treturn -1;\n+\t\t\tret = -1;\n+\t\t\tgoto cleanup;\n \t\t}\n \t}\n \n \tcommit_lock_file(&lk);\n \n-\treturn 0;\n+cleanup:\n+\tfree(chunks);\n+\treturn ret;\n }\n \n static void split_graph_merge_strategy(struct write_commit_graph_context *ctx)\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398853","messageId":"20200529085038.26008-13-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 12/34] commit-graph: unify the signatures of all write_graph_chunk_*() functions","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:16Z","receivedAt":"2020-05-29T08:51:22Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Update the write_graph_chunk_*() helper functions to have the same\nsignature:\n\n  - Return an int error code from all these functions.\n    write_graph_chunk_base() already has an int error code, now the\n    others will have one, too, but since they don't indicate any\n    error, they will always return 0.\n\n  - Drop the hash size parameter of write_graph_chunk_oids() and\n    write_graph_chunk_data(); its value can be read directly from\n    'the_hash_algo' inside these functions as well.\n\nThis opens up the possibility for further cleanups and foolproofing in\nthe following two patches.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 20 ++++++++++++--------\n 1 file changed, 12 insertions(+), 8 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex c8ba17e457..33e70bdfea 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -793,7 +793,7 @@ struct write_commit_graph_context {\n \tconst struct split_commit_graph_opts *split_opts;\n };\n \n-static void write_graph_chunk_fanout(struct hashfile *f,\n+static int write_graph_chunk_fanout(struct hashfile *f,\n \t\t\t\t     struct write_commit_graph_context *ctx)\n {\n \tint i, count = 0;\n@@ -815,17 +815,19 @@ static void write_graph_chunk_fanout(struct hashfile *f,\n \n \t\thashwrite_be32(f, count);\n \t}\n+\treturn 0;\n }\n \n-static void write_graph_chunk_oids(struct hashfile *f, int hash_len,\n+static int write_graph_chunk_oids(struct hashfile *f,\n \t\t\t\t   struct write_commit_graph_context *ctx)\n {\n \tstruct commit **list = ctx->commits.list;\n \tint count;\n \tfor (count = 0; count < ctx->commits.nr; count++, list++) {\n \t\tdisplay_progress(ctx->progress, ++ctx->progress_cnt);\n-\t\thashwrite(f, (*list)->object.oid.hash, (int)hash_len);\n+\t\thashwrite(f, (*list)->object.oid.hash, the_hash_algo->rawsz);\n \t}\n+\treturn 0;\n }\n \n static const unsigned char *commit_to_sha1(size_t index, void *table)\n@@ -834,7 +836,7 @@ static const unsigned char *commit_to_sha1(size_t index, void *table)\n \treturn commits[index]->object.oid.hash;\n }\n \n-static void write_graph_chunk_data(struct hashfile *f, int hash_len,\n+static int write_graph_chunk_data(struct hashfile *f,\n \t\t\t\t   struct write_commit_graph_context *ctx)\n {\n \tstruct commit **list = ctx->commits.list;\n@@ -852,7 +854,7 @@ static void write_graph_chunk_data(struct hashfile *f, int hash_len,\n \t\t\tdie(_(\"unable to parse commit %s\"),\n \t\t\t\toid_to_hex(&(*list)->object.oid));\n \t\ttree = get_commit_tree_oid(*list);\n-\t\thashwrite(f, tree->hash, hash_len);\n+\t\thashwrite(f, tree->hash, the_hash_algo->rawsz);\n \n \t\tparent = (*list)->parents;\n \n@@ -932,9 +934,10 @@ static void write_graph_chunk_data(struct hashfile *f, int hash_len,\n \n \t\tlist++;\n \t}\n+\treturn 0;\n }\n \n-static void write_graph_chunk_extra_edges(struct hashfile *f,\n+static int write_graph_chunk_extra_edges(struct hashfile *f,\n \t\t\t\t\t  struct write_commit_graph_context *ctx)\n {\n \tstruct commit **list = ctx->commits.list;\n@@ -984,6 +987,7 @@ static void write_graph_chunk_extra_edges(struct hashfile *f,\n \n \t\tlist++;\n \t}\n+\treturn 0;\n }\n \n static int oid_compare(const void *_a, const void *_b)\n@@ -1462,8 +1466,8 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\t\tchunks_nr * ctx->commits.nr);\n \t}\n \twrite_graph_chunk_fanout(f, ctx);\n-\twrite_graph_chunk_oids(f, hashsz, ctx);\n-\twrite_graph_chunk_data(f, hashsz, ctx);\n+\twrite_graph_chunk_oids(f, ctx);\n+\twrite_graph_chunk_data(f, ctx);\n \tif (ctx->num_extra_edges)\n \t\twrite_graph_chunk_extra_edges(f, ctx);\n \tif (ctx->num_commit_graphs_after > 1 &&\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398854","messageId":"20200529085038.26008-14-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 13/34] commit-graph: simplify write_commit_graph_file() #3","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:17Z","receivedAt":"2020-05-29T08:51:23Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"In write_commit_graph_file() we now have one block of code filling the\narray of 'struct chunk_info' with the IDs and sizes of chunks to be\nwritten, and an other block of code calling the functions responsible\nfor writing individual chunks.  In case of optional chunks like Extra\nEdge List an Base Graphs List there is also a condition checking\nwhether that chunk is necessary/desired, and that same condition is\nrepeated in both blocks of code.  This patch series is about to add\nmore optional chunks, so there would be even more repeated conditions.\n\nEliminate these repeated conditions by storing the function pointers\nresponsible for writing individual chunks in the 'struct chunk_info'\narray as well, and calling them in a loop to write the commit-graph\nfile.  This will open up the possibility for a bit of foolproofing in\nthe following patch.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 24 ++++++++++++++----------\n 1 file changed, 14 insertions(+), 10 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 33e70bdfea..fe26245fad 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1348,9 +1348,12 @@ static int write_graph_chunk_base(struct hashfile *f,\n \treturn 0;\n }\n \n+typedef int (*chunk_write_fn)(struct hashfile *f,\n+\t\t\t      struct write_commit_graph_context *ctx);\n struct chunk_info {\n \tuint32_t id;\n \tuint64_t size;\n+\tchunk_write_fn write_fn;\n };\n \n static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n@@ -1406,31 +1409,37 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \tchunks[chunks_nr].id = GRAPH_CHUNKID_OIDFANOUT;\n \tchunks[chunks_nr].size = GRAPH_FANOUT_SIZE;\n+\tchunks[chunks_nr].write_fn = write_graph_chunk_fanout;\n \tchunks_nr++;\n \tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \tchunks[chunks_nr].id = GRAPH_CHUNKID_OIDLOOKUP;\n \tchunks[chunks_nr].size = hashsz * ctx->commits.nr;\n+\tchunks[chunks_nr].write_fn = write_graph_chunk_oids;\n \tchunks_nr++;\n \tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \tchunks[chunks_nr].id = GRAPH_CHUNKID_DATA;\n \tchunks[chunks_nr].size = (hashsz + 16) * ctx->commits.nr;\n+\tchunks[chunks_nr].write_fn = write_graph_chunk_data;\n \tchunks_nr++;\n \tif (ctx->num_extra_edges) {\n \t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \t\tchunks[chunks_nr].id = GRAPH_CHUNKID_EXTRAEDGES;\n \t\tchunks[chunks_nr].size = 4 * ctx->num_extra_edges;\n+\t\tchunks[chunks_nr].write_fn = write_graph_chunk_extra_edges;\n \t\tchunks_nr++;\n \t}\n \tif (ctx->num_commit_graphs_after > 1) {\n \t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \t\tchunks[chunks_nr].id = GRAPH_CHUNKID_BASE;\n \t\tchunks[chunks_nr].size = hashsz * (ctx->num_commit_graphs_after - 1);\n+\t\tchunks[chunks_nr].write_fn = write_graph_chunk_base;\n \t\tchunks_nr++;\n \t}\n \n \tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \tchunks[chunks_nr].id = 0;\n \tchunks[chunks_nr].size = 0;\n+\tchunks[chunks_nr].write_fn = NULL;\n \t/*\n \t * Do not increase 'chunks_nr', so it still reflects the number of\n \t * actual chunks, without the Chunk Lookup table's terminating label.\n@@ -1465,16 +1474,11 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\t\tprogress_title.buf,\n \t\t\tchunks_nr * ctx->commits.nr);\n \t}\n-\twrite_graph_chunk_fanout(f, ctx);\n-\twrite_graph_chunk_oids(f, ctx);\n-\twrite_graph_chunk_data(f, ctx);\n-\tif (ctx->num_extra_edges)\n-\t\twrite_graph_chunk_extra_edges(f, ctx);\n-\tif (ctx->num_commit_graphs_after > 1 &&\n-\t    write_graph_chunk_base(f, ctx)) {\n-\t\tret = -1;\n-\t\tgoto cleanup;\n-\t}\n+\tfor (i = 0; i < chunks_nr; i++)\n+\t\tif (chunks[i].write_fn(f, ctx)) {\n+\t\t\tret = -1;\n+\t\t\tgoto cleanup;\n+\t\t}\n \tstop_progress(&ctx->progress);\n \tstrbuf_release(&progress_title);\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398855","messageId":"20200529085038.26008-15-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 14/34] commit-graph: check chunk sizes after writing","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:18Z","receivedAt":"2020-05-29T08:51:24Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"In my experience while experimenting with new commit-graph chunks,\nearly versions of the corresponding new write_commit_graph_my_chunk()\nfunctions are, sadly but not surprisingly, often buggy, and write more\nor less data than they are supposed to, especially if the chunk size\nis not directly proportional to the number of commits.  This then\ncauses all kinds of issues when reading such a bogus commit-graph\nfile, raising the question of whether the writing or the reading part\nhappens to be buggy this time.\n\nLet's catch such issues early, already when writing the commit-graph\nfile, and check that each write_commit_graph_*() function wrote the\namount of data that it was expected to, and what has been encoded in\nthe Chunk Lookup table.  Now that all commit-graph chunks are written\nin a loop we can do this check in a single place for all chunks, and\nany chunks added in the future will get checked as well.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 13 ++++++++++++-\n 1 file changed, 12 insertions(+), 1 deletion(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex fe26245fad..e3adffd58b 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1474,11 +1474,22 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\t\tprogress_title.buf,\n \t\t\tchunks_nr * ctx->commits.nr);\n \t}\n-\tfor (i = 0; i < chunks_nr; i++)\n+\tchunk_offset = f->total + f->offset;\n+\tfor (i = 0; i < chunks_nr; i++) {\n+\t\tuint64_t end_offset;\n+\n \t\tif (chunks[i].write_fn(f, ctx)) {\n \t\t\tret = -1;\n \t\t\tgoto cleanup;\n \t\t}\n+\n+\t\tend_offset = f->total + f->offset;\n+\t\tif (end_offset - chunk_offset != chunks[i].size)\n+\t\t\tBUG(\"expected to write %lu bytes to chunk %08x, but wrote %lu instead\",\n+\t\t\t    chunks[i].size, chunks[i].id,\n+\t\t\t    end_offset - chunk_offset);\n+\t\tchunk_offset = end_offset;\n+\t}\n \tstop_progress(&ctx->progress);\n \tstrbuf_release(&progress_title);\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398856","messageId":"20200529085038.26008-17-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 16/34] Add a generic and minimal Bloom filter implementation","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:20Z","receivedAt":"2020-05-29T08:51:26Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"We'll use Bloom filters to store the paths modified between each\ncommit and one of its parents.  This means many small Bloom filters,\nwhich puts some constraints on the hashing scheme, as discussed in the\nprevious commit.  However, Bloom filters might turn out to be useful\nin other parts of Git as well, where they might be free from those\nconstraints.  So add a Bloom filter implementation that is fairly\ngeneric, and copes only with allocation, initialization and release of\nthe filter and with manipulating the bits in its bit array, but leaves\nall the hashing to the user of the filter.\n\nUnfortunately, it's still somewhat specific, though, in how it maps\nthe hashes to specific bits in memory, i.e. how it maps the hashes to\narray indices and how the bit array is indexed.  We'll see how that\nwill pan out...\n---\n Makefile       |  1 +\n bloom-filter.c | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++\n bloom-filter.h | 47 ++++++++++++++++++++++++++\n 3 files changed, 139 insertions(+)\n create mode 100644 bloom-filter.c\n create mode 100644 bloom-filter.h\n\ndiff --git a/Makefile b/Makefile\nindex 09f98b777c..a9db1e8d9b 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -839,6 +839,7 @@ LIB_OBJS += base85.o\n LIB_OBJS += bisect.o\n LIB_OBJS += blame.o\n LIB_OBJS += blob.o\n+LIB_OBJS += bloom-filter.o\n LIB_OBJS += branch.o\n LIB_OBJS += bulk-checkin.o\n LIB_OBJS += bundle.o\ndiff --git a/bloom-filter.c b/bloom-filter.c\nnew file mode 100644\nindex 0000000000..919da26462\n--- /dev/null\n+++ b/bloom-filter.c\n@@ -0,0 +1,91 @@\n+#include \"bloom-filter.h\"\n+\n+void bloom_filter_init(struct bloom_filter *bf, unsigned int nr_hashes,\n+\t\t       uint32_t nr_elements)\n+{\n+\t/* n * k / ln(2) ≈ n * k / 0.69315 ≈ n * k * 10 / 7 */\n+\tuint32_t nr_bits = st_mult(st_mult(nr_elements, nr_hashes), 10) / 7;\n+\tuint32_t nr_bytes = st_add(nr_bits, 7) / 8;\n+\t/*\n+\t * But we round up to fully utilize all bytes, thus lowering the\n+\t * probability of false positives a bit.\n+\t */\n+\tbf->nr_bits = nr_bytes * 8;\n+\tbf->bits = xcalloc(nr_bytes, sizeof(*(bf->bits)));\n+}\n+\n+void bloom_filter_init_with_size(struct bloom_filter *bf, uint32_t nr_bits)\n+{\n+\tuint32_t nr_bytes = st_add(nr_bits, 7) / 8;\n+\tbf->nr_bits = nr_bits;\n+\tbf->bits = xcalloc(nr_bytes, sizeof(*(bf->bits)));\n+}\n+\n+void bloom_filter_free(struct bloom_filter *bf)\n+{\n+\tFREE_AND_NULL(bf->bits);\n+\tbf->nr_bits = 0;\n+}\n+\n+uint32_t bloom_filter_bytes(struct bloom_filter *bf)\n+{\n+\treturn (bf->nr_bits + 7) / 8;\n+}\n+\n+void bloom_filter_clear_all_bits(struct bloom_filter *bf)\n+{\n+\tmemset(bf->bits, 0, bloom_filter_bytes(bf));\n+}\n+\n+void bloom_filter_set_all_bits(struct bloom_filter *bf)\n+{\n+\tmemset(bf->bits, 0xff, bloom_filter_bytes(bf));\n+}\n+\n+static inline uint32_t bit_offset(uint32_t nr_bits, uint32_t hash)\n+{\n+\treturn hash % nr_bits;\n+}\n+\n+static inline uint32_t byte_offset(uint32_t nr_bits, uint32_t bit_offset)\n+{\n+\treturn (nr_bits - 1) / 8 - bit_offset / 8;\n+}\n+\n+static inline uint8_t byte_mask(uint32_t bit_offset)\n+{\n+\treturn 1 << (bit_offset % 8);\n+}\n+\n+static inline void bloom_filter_set_one_bit(struct bloom_filter *bf,\n+\t\t\t\t\t    uint32_t hash)\n+{\n+\tuint32_t offset = bit_offset(bf->nr_bits, hash);\n+\tbf->bits[byte_offset(bf->nr_bits, offset)] |= byte_mask(offset);\n+}\n+\n+void bloom_filter_set_bits(struct bloom_filter *bf, const uint32_t *hashes,\n+\t\t\t   unsigned int nr_hashes)\n+{\n+\tunsigned int i;\n+\tfor (i = 0; i < nr_hashes; i++)\n+\t\tbloom_filter_set_one_bit(bf, hashes[i]);\n+}\n+\n+static inline int bloom_filter_check_one_bit(struct bloom_filter *bf,\n+\t\t\t\t\t     uint32_t hash)\n+{\n+\tuint32_t offset = bit_offset(bf->nr_bits, hash);\n+\treturn bf->bits[byte_offset(bf->nr_bits, offset)] & byte_mask(offset);\n+}\n+\n+enum bloom_result bloom_filter_check_bits(struct bloom_filter *bf,\n+\t\t\t\t\t  const uint32_t *hashes,\n+\t\t\t\t\t  unsigned int nr_hashes)\n+{\n+\tunsigned int i;\n+\tfor (i = 0; i < nr_hashes; i++)\n+\t\tif (!bloom_filter_check_one_bit(bf, hashes[i]))\n+\t\t\treturn BLOOM_DEFINITELY_NOT;\n+\treturn BLOOM_POSSIBLY_YES;\n+}\ndiff --git a/bloom-filter.h b/bloom-filter.h\nnew file mode 100644\nindex 0000000000..2fcd4a2c19\n--- /dev/null\n+++ b/bloom-filter.h\n@@ -0,0 +1,47 @@\n+#ifndef BLOOM_FILTER_H\n+#define BLOOM_FILTER_H\n+\n+#include \"git-compat-util.h\"\n+\n+enum bloom_result {\n+\t/*\n+\t * BLOOM_CANT_TELL is a value that a caller can use to report that\n+\t * a Bloom filter is not available; bloom_filter_check_bits() will\n+\t * never return it.\n+\t */\n+\tBLOOM_CANT_TELL = -1,\n+\tBLOOM_DEFINITELY_NOT = 0,\n+\tBLOOM_POSSIBLY_YES = 1\n+};\n+\n+struct bloom_filter {\n+\tuint32_t nr_bits;\n+\tuint8_t *bits;\n+};\n+\n+/*\n+ * Initialize a Bloom filter with the number of bits that is (close to)\n+ * optimal to hold the given number of elements using the given number\n+ * of hashes per element.\n+ */\n+void bloom_filter_init(struct bloom_filter *bf, uint32_t nr_hashes,\n+\t\t       uint32_t nr_elements);\n+\n+/* Initialize a Bloom filter with the given number of bits */\n+void bloom_filter_init_with_size(struct bloom_filter *bf, uint32_t nr_bits);\n+\n+void bloom_filter_free(struct bloom_filter *bf);\n+\n+/* Return the size of the Bloom filter's bit array in bytes */\n+uint32_t bloom_filter_bytes(struct bloom_filter *bf);\n+\n+void bloom_filter_clear_all_bits(struct bloom_filter *bf);\n+void bloom_filter_set_all_bits(struct bloom_filter *bf);\n+\n+void bloom_filter_set_bits(struct bloom_filter *bf, const uint32_t *hashes,\n+\t\t\t   unsigned int nr_hashes);\n+enum bloom_result bloom_filter_check_bits(struct bloom_filter *bf,\n+\t\t\t\t\t  const uint32_t *hashes,\n+\t\t\t\t\t  unsigned int nr_hashes);\n+\n+#endif\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398857","messageId":"20200529085038.26008-18-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 17/34] Import a streaming-capable Murmur3 hash function implementation","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:21Z","receivedAt":"2020-05-29T08:51:29Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Import the streaming-capable public domain Murmur3 hash function\nimplementation from https://github.com/aappleby/smhasher/.\nIt's imported as-is, with 2 space indentation (the horror!) and\nwhitespace errors...\n\nThe reason for importing the streaming-capable implementation is that we\nmight get some benefit when hashing the paths 'dir', 'dir/subdir',\n'dir/subdir/file' one ofter the other.\n---\n Makefile          |   1 +\n compat/PMurHash.c | 291 ++++++++++++++++++++++++++++++++++++++++++++++\n compat/PMurHash.h |  62 ++++++++++\n 3 files changed, 354 insertions(+)\n create mode 100644 compat/PMurHash.c\n create mode 100644 compat/PMurHash.h\n\ndiff --git a/Makefile b/Makefile\nindex a9db1e8d9b..1338f10796 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -853,6 +853,7 @@ LIB_OBJS += commit.o\n LIB_OBJS += commit-graph.o\n LIB_OBJS += commit-reach.o\n LIB_OBJS += compat/obstack.o\n+LIB_OBJS += compat/PMurHash.o\n LIB_OBJS += compat/terminal.o\n LIB_OBJS += config.o\n LIB_OBJS += connect.o\ndiff --git a/compat/PMurHash.c b/compat/PMurHash.c\nnew file mode 100644\nindex 0000000000..df8842aa29\n--- /dev/null\n+++ b/compat/PMurHash.c\n@@ -0,0 +1,291 @@\n+/*-----------------------------------------------------------------------------\n+ * MurmurHash3 was written by Austin Appleby, and is placed in the public\n+ * domain.\n+ *\n+ * This implementation was written by Shane Day, and is also public domain.\n+ *\n+ * This is a portable ANSI C implementation of MurmurHash3_x86_32 (Murmur3A)\n+ * with support for progressive processing.\n+ */\n+\n+/*-----------------------------------------------------------------------------\n+ \n+If you want to understand the MurmurHash algorithm you would be much better\n+off reading the original source. Just point your browser at:\n+http://code.google.com/p/smhasher/source/browse/trunk/MurmurHash3.cpp\n+\n+\n+What this version provides?\n+\n+1. Progressive data feeding. Useful when the entire payload to be hashed\n+does not fit in memory or when the data is streamed through the application.\n+Also useful when hashing a number of strings with a common prefix. A partial\n+hash of a prefix string can be generated and reused for each suffix string.\n+\n+2. Portability. Plain old C so that it should compile on any old compiler.\n+Both CPU endian and access-alignment neutral, but avoiding inefficient code\n+when possible depending on CPU capabilities.\n+\n+3. Drop in. I personally like nice self contained public domain code, making it\n+easy to pilfer without loads of refactoring to work properly in the existing\n+application code & makefile structure and mucking around with licence files.\n+Just copy PMurHash.h and PMurHash.c and you're ready to go.\n+\n+\n+How does it work?\n+\n+We can only process entire 32 bit chunks of input, except for the very end\n+that may be shorter. So along with the partial hash we need to give back to\n+the caller a carry containing up to 3 bytes that we were unable to process.\n+This carry also needs to record the number of bytes the carry holds. I use\n+the low 2 bits as a count (0..3) and the carry bytes are shifted into the\n+high byte in stream order.\n+\n+To handle endianess I simply use a macro that reads a uint32_t and define\n+that macro to be a direct read on little endian machines, a read and swap\n+on big endian machines, or a byte-by-byte read if the endianess is unknown.\n+\n+-----------------------------------------------------------------------------*/\n+\n+\n+#include \"PMurHash.h\"\n+\n+/* I used ugly type names in the header to avoid potential conflicts with\n+ * application or system typedefs & defines. Since I'm not including any more\n+ * headers below here I can rename these so that the code reads like C99 */\n+#undef uint32_t\n+#define uint32_t MH_UINT32\n+#undef uint8_t\n+#define uint8_t  MH_UINT8\n+\n+/* MSVC warnings we choose to ignore */\n+#if defined(_MSC_VER)\n+  #pragma warning(disable: 4127) /* conditional expression is constant */\n+#endif\n+\n+/*-----------------------------------------------------------------------------\n+ * Endianess, misalignment capabilities and util macros\n+ *\n+ * The following 3 macros are defined in this section. The other macros defined\n+ * are only needed to help derive these 3.\n+ *\n+ * READ_UINT32(x)   Read a little endian unsigned 32-bit int\n+ * UNALIGNED_SAFE   Defined if READ_UINT32 works on non-word boundaries\n+ * ROTL32(x,r)      Rotate x left by r bits\n+ */\n+\n+/* Convention is to define __BYTE_ORDER == to one of these values */\n+#if !defined(__BIG_ENDIAN)\n+  #define __BIG_ENDIAN 4321\n+#endif\n+#if !defined(__LITTLE_ENDIAN)\n+  #define __LITTLE_ENDIAN 1234\n+#endif\n+\n+/* I386 */\n+#if defined(_M_IX86) || defined(__i386__) || defined(__i386) || defined(i386)\n+  #define __BYTE_ORDER __LITTLE_ENDIAN\n+  #define UNALIGNED_SAFE\n+#endif\n+\n+/* gcc 'may' define __LITTLE_ENDIAN__ or __BIG_ENDIAN__ to 1 (Note the trailing __),\n+ * or even _LITTLE_ENDIAN or _BIG_ENDIAN (Note the single _ prefix) */\n+#if !defined(__BYTE_ORDER)\n+  #if defined(__LITTLE_ENDIAN__) && __LITTLE_ENDIAN__==1 || defined(_LITTLE_ENDIAN) && _LITTLE_ENDIAN==1\n+    #define __BYTE_ORDER __LITTLE_ENDIAN\n+  #elif defined(__BIG_ENDIAN__) && __BIG_ENDIAN__==1 || defined(_BIG_ENDIAN) && _BIG_ENDIAN==1\n+    #define __BYTE_ORDER __BIG_ENDIAN\n+  #endif\n+#endif\n+\n+/* gcc (usually) defines xEL/EB macros for ARM and MIPS endianess */\n+#if !defined(__BYTE_ORDER)\n+  #if defined(__ARMEL__) || defined(__MIPSEL__)\n+    #define __BYTE_ORDER __LITTLE_ENDIAN\n+  #endif\n+  #if defined(__ARMEB__) || defined(__MIPSEB__)\n+    #define __BYTE_ORDER __BIG_ENDIAN\n+  #endif\n+#endif\n+\n+/* Now find best way we can to READ_UINT32 */\n+#if __BYTE_ORDER==__LITTLE_ENDIAN\n+  /* CPU endian matches murmurhash algorithm, so read 32-bit word directly */\n+  #define READ_UINT32(ptr)   (*((uint32_t*)(ptr)))\n+#elif __BYTE_ORDER==__BIG_ENDIAN\n+  /* TODO: Add additional cases below where a compiler provided bswap32 is available */\n+  #if defined(__GNUC__) && (__GNUC__>4 || (__GNUC__==4 && __GNUC_MINOR__>=3))\n+    #define READ_UINT32(ptr)   (__builtin_bswap32(*((uint32_t*)(ptr))))\n+  #else\n+    /* Without a known fast bswap32 we're just as well off doing this */\n+    #define READ_UINT32(ptr)   (ptr[0]|ptr[1]<<8|ptr[2]<<16|ptr[3]<<24)\n+    #define UNALIGNED_SAFE\n+  #endif\n+#else\n+  /* Unknown endianess so last resort is to read individual bytes */\n+  #define READ_UINT32(ptr)   (ptr[0]|ptr[1]<<8|ptr[2]<<16|ptr[3]<<24)\n+\n+  /* Since we're not doing word-reads we can skip the messing about with realignment */\n+  #define UNALIGNED_SAFE\n+#endif\n+\n+/* Find best way to ROTL32 */\n+#if defined(_MSC_VER)\n+  #include <stdlib.h>  /* Microsoft put _rotl declaration in here */\n+  #define ROTL32(x,r)  _rotl(x,r)\n+#else\n+  /* gcc recognises this code and generates a rotate instruction for CPUs with one */\n+  #define ROTL32(x,r)  (((uint32_t)x << r) | ((uint32_t)x >> (32 - r)))\n+#endif\n+\n+\n+/*-----------------------------------------------------------------------------\n+ * Core murmurhash algorithm macros */\n+\n+#define C1  (0xcc9e2d51)\n+#define C2  (0x1b873593)\n+\n+/* This is the main processing body of the algorithm. It operates\n+ * on each full 32-bits of input. */\n+#define DOBLOCK(h1, k1) do{ \\\n+        k1 *= C1; \\\n+        k1 = ROTL32(k1,15); \\\n+        k1 *= C2; \\\n+        \\\n+        h1 ^= k1; \\\n+        h1 = ROTL32(h1,13); \\\n+        h1 = h1*5+0xe6546b64; \\\n+    }while(0)\n+\n+\n+/* Append unaligned bytes to carry, forcing hash churn if we have 4 bytes */\n+/* cnt=bytes to process, h1=name of h1 var, c=carry, n=bytes in c, ptr/len=payload */\n+#define DOBYTES(cnt, h1, c, n, ptr, len) do{ \\\n+    int _i = cnt; \\\n+    while(_i--) { \\\n+        c = c>>8 | *ptr++<<24; \\\n+        n++; len--; \\\n+        if(n==4) { \\\n+            DOBLOCK(h1, c); \\\n+            n = 0; \\\n+        } \\\n+    } }while(0)\n+\n+/*---------------------------------------------------------------------------*/\n+\n+/* Main hashing function. Initialise carry to 0 and h1 to 0 or an initial seed\n+ * if wanted. Both ph1 and pcarry are required arguments. */\n+void PMurHash32_Process(uint32_t *ph1, uint32_t *pcarry, const void *key, int len)\n+{\n+  uint32_t h1 = *ph1;\n+  uint32_t c = *pcarry;\n+\n+  const uint8_t *ptr = (uint8_t*)key;\n+  const uint8_t *end;\n+\n+  /* Extract carry count from low 2 bits of c value */\n+  int n = c & 3;\n+\n+#if defined(UNALIGNED_SAFE)\n+  /* This CPU handles unaligned word access */\n+\n+  /* Consume any carry bytes */\n+  int i = (4-n) & 3;\n+  if(i && i <= len) {\n+    DOBYTES(i, h1, c, n, ptr, len);\n+  }\n+\n+  /* Process 32-bit chunks */\n+  end = ptr + len/4*4;\n+  for( ; ptr < end ; ptr+=4) {\n+    uint32_t k1 = READ_UINT32(ptr);\n+    DOBLOCK(h1, k1);\n+  }\n+\n+#else /*UNALIGNED_SAFE*/\n+  /* This CPU does not handle unaligned word access */\n+\n+  /* Consume enough so that the next data byte is word aligned */\n+  int i = -(long)ptr & 3;\n+  if(i && i <= len) {\n+      DOBYTES(i, h1, c, n, ptr, len);\n+  }\n+\n+  /* We're now aligned. Process in aligned blocks. Specialise for each possible carry count */\n+  end = ptr + len/4*4;\n+  switch(n) { /* how many bytes in c */\n+  case 0: /* c=[----]  w=[3210]  b=[3210]=w            c'=[----] */\n+    for( ; ptr < end ; ptr+=4) {\n+      uint32_t k1 = READ_UINT32(ptr);\n+      DOBLOCK(h1, k1);\n+    }\n+    break;\n+  case 1: /* c=[0---]  w=[4321]  b=[3210]=c>>24|w<<8   c'=[4---] */\n+    for( ; ptr < end ; ptr+=4) {\n+      uint32_t k1 = c>>24;\n+      c = READ_UINT32(ptr);\n+      k1 |= c<<8;\n+      DOBLOCK(h1, k1);\n+    }\n+    break;\n+  case 2: /* c=[10--]  w=[5432]  b=[3210]=c>>16|w<<16  c'=[54--] */\n+    for( ; ptr < end ; ptr+=4) {\n+      uint32_t k1 = c>>16;\n+      c = READ_UINT32(ptr);\n+      k1 |= c<<16;\n+      DOBLOCK(h1, k1);\n+    }\n+    break;\n+  case 3: /* c=[210-]  w=[6543]  b=[3210]=c>>8|w<<24   c'=[654-] */\n+    for( ; ptr < end ; ptr+=4) {\n+      uint32_t k1 = c>>8;\n+      c = READ_UINT32(ptr);\n+      k1 |= c<<24;\n+      DOBLOCK(h1, k1);\n+    }\n+  }\n+#endif /*UNALIGNED_SAFE*/\n+\n+  /* Advance over whole 32-bit chunks, possibly leaving 1..3 bytes */\n+  len -= len/4*4;\n+\n+  /* Append any remaining bytes into carry */\n+  DOBYTES(len, h1, c, n, ptr, len);\n+\n+  /* Copy out new running hash and carry */\n+  *ph1 = h1;\n+  *pcarry = (c & ~0xff) | n;\n+} \n+\n+/*---------------------------------------------------------------------------*/\n+\n+/* Finalize a hash. To match the original Murmur3A the total_length must be provided */\n+uint32_t PMurHash32_Result(uint32_t h, uint32_t carry, uint32_t total_length)\n+{\n+  uint32_t k1;\n+  int n = carry & 3;\n+  if(n) {\n+    k1 = carry >> (4-n)*8;\n+    k1 *= C1; k1 = ROTL32(k1,15); k1 *= C2; h ^= k1;\n+  }\n+  h ^= total_length;\n+\n+  /* fmix */\n+  h ^= h >> 16;\n+  h *= 0x85ebca6b;\n+  h ^= h >> 13;\n+  h *= 0xc2b2ae35;\n+  h ^= h >> 16;\n+\n+  return h;\n+}\n+\n+/*---------------------------------------------------------------------------*/\n+\n+/* Murmur3A compatable all-at-once */\n+uint32_t PMurHash32(uint32_t seed, const void *key, int len)\n+{\n+  uint32_t h1=seed, carry=0;\n+  PMurHash32_Process(&h1, &carry, key, len);\n+  return PMurHash32_Result(h1, carry, len);\n+}\ndiff --git a/compat/PMurHash.h b/compat/PMurHash.h\nnew file mode 100644\nindex 0000000000..0ee645669e\n--- /dev/null\n+++ b/compat/PMurHash.h\n@@ -0,0 +1,62 @@\n+/*-----------------------------------------------------------------------------\n+ * MurmurHash3 was written by Austin Appleby, and is placed in the public\n+ * domain.\n+ *\n+ * This implementation was written by Shane Day, and is also public domain.\n+ *\n+ * This is a portable ANSI C implementation of MurmurHash3_x86_32 (Murmur3A)\n+ * with support for progressive processing.\n+ */\n+\n+/* ------------------------------------------------------------------------- */\n+/* Determine what native type to use for uint32_t */\n+\n+/* We can't use the name 'uint32_t' here because it will conflict with\n+ * any version provided by the system headers or application. */\n+\n+/* First look for special cases */\n+#if defined(_MSC_VER)\n+  #define MH_UINT32 unsigned long\n+#endif\n+\n+/* If the compiler says it's C99 then take its word for it */\n+#if !defined(MH_UINT32) && ( \\\n+     defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L )\n+  #include <stdint.h>\n+  #define MH_UINT32 uint32_t\n+#endif\n+\n+/* Otherwise try testing against max value macros from limit.h */\n+#if !defined(MH_UINT32)\n+  #include  <limits.h>\n+  #if   (USHRT_MAX == 0xffffffffUL)\n+    #define MH_UINT32 unsigned short\n+  #elif (UINT_MAX == 0xffffffffUL)\n+    #define MH_UINT32 unsigned int\n+  #elif (ULONG_MAX == 0xffffffffUL)\n+    #define MH_UINT32 unsigned long\n+  #endif\n+#endif\n+\n+#if !defined(MH_UINT32)\n+  #error Unable to determine type name for unsigned 32-bit int\n+#endif\n+\n+/* I'm yet to work on a platform where 'unsigned char' is not 8 bits */\n+#define MH_UINT8  unsigned char\n+\n+\n+/* ------------------------------------------------------------------------- */\n+/* Prototypes */\n+\n+#ifdef __cplusplus\n+extern \"C\" {\n+#endif\n+\n+void PMurHash32_Process(MH_UINT32 *ph1, MH_UINT32 *pcarry, const void *key, int len);\n+MH_UINT32 PMurHash32_Result(MH_UINT32 h1, MH_UINT32 carry, MH_UINT32 total_length);\n+MH_UINT32 PMurHash32(MH_UINT32 seed, const void *key, int len);\n+\n+#ifdef __cplusplus\n+}\n+#endif\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398860","messageId":"20200529085038.26008-16-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 15/34] commit-graph-format.txt: document the modified path Bloom filter chunks","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:19Z","receivedAt":"2020-05-29T08:51:30Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"During a pathspec-limited revision walk, e.g. 'git log -- dir/file',\nGit spends a significant part of its runtime in the tree-diff\nmachinery, checking whether the given path was modified between\nsubsequent commits.  By using Bloom filters to store the paths\nmodified by each commit we can quickly tell that a commit didn't\nmodify the given path without invoking tree-diff, thus considerably\nreduce this diffing overhead, along with the runtime and memory usage.\nThis patch extends the commit-graph file format with additional chunks\nto store those modified path Bloom filters.\n\nThe rest of this log message takes a closer look at the problem and\nexplains why these new chunks look like they do.\n\nIn the following the terms \"file\" and \"path\" are not used\ninterchangeably.  If a commit modified 'dir/subdir/foo.c', then it\nmodified that one file, but it modified three paths (namely 'dir',\n'dir/subdir', and 'dir/subdir/foo.c').\n\nFurthermore, unless otherwise noted, \"a lot of paths\" means 5000\nrandomly selected paths that have existed at one time or another\nduring the history of the corresponding repository's default branch\n[1], except for git it means all 5089 paths that have ever existed\nduring the history of v2.25.0, and for homebrew-cask and homebrew-core\nall 7307 and 6535 paths, respectively, that have ever existed in the\nhistory of their default branches some time ago.\n\nAbout pathspec-limited revision walks\n-------------------------------------\n\nSo, during a pathspec-limited revision walk Git spends a significant\npart of its runtime in the tree-diff machinery, checking whether the\ngiven path was modified between subsequent commits.  The table below\nshows the average runtime of 'git rev-list HEAD -- $path' for \"a lot\nof paths\" in a couple of repositories, and how much of that runtime is\nspent in diff_tree_oid().  It also shows the potential average\nspeedup, should we be able to reduce the tree-diff overhead to zero\nwithout introducing some other overhead (spoiler alert: we won't be\nable to achieve that, of course, but still will achieve even higher\naverage speedup in several cases).\n\n                 Average    Average            Potential\n                 runtime   diff time            speedup\n  -------------------------------------------------------\n  android-base   0.8780s    0.7320s    83.37%     6.14x\n  cmssw          0.3143s    0.2800s    88.95%     9.19x\n  cpython        0.7453s    0.6602s    88.58%     8.76x\n  elasticsearch  0.1492s    0.1351s    90.55%    10.58x\n  gcc            7.1852s    6.9432s    96.63%    29.69x\n  gecko-dev      4.6113s    4.0964s    88.83%     8.96x\n  git            0.6180s    0.5911s    95.65%    22.97x\n  glibc          0.5618s    0.5313s    94.57%    18.42x\n  go             0.4913s    0.4469s    90.96%    11.07x\n  jdk            0.0482s    0.0431s    89.42%     9.45x\n  linux          0.7043s    0.6163s    87.50%     8.00x\n  llvm-project   2.6844s    2.2607s    84.22%     6.34x\n  rails          0.2784s    0.2372s    85.20%     6.76x\n  rust           0.7757s    0.7349s    94.74%    19.01x\n  tensorflow     0.6258s    0.5735s    91.64%    11.96x\n  webkit         1.9137s    1.6580s    86.64%     7.48x\n\nNotice that the average time spent diffing in the Linux and Git\nrepositories is quite close (0.6163s vs 0.5911s), although the Linux\nrepository contains about 15x more commits and 25x more files; more on\nthis later.\n\nInstrumenting the tree-diff machinery and gathering some stats about\nthe number of diff calls, tree object comparisons and decoded tree\nentries revealed some interesting, perhaps even counterintuitive\nthings:\n\n  - The number of scanned tree entries can be more important than the\n    number of tree comparisons.\n\n    Here are four paths from two repositories, two each in the same\n    frequently-modified directory, with one near the beginning and one\n    at the end of that directory.  When listing the commits modifying\n    these paths, i.e. 'git rev-list HEAD -- $path', the number of tree\n    comparisons is (nearly) the same, but the number of scanned tree\n    entries differs by over two orders of magnitude.\n\n                                      Average   Nr of scanned   Nr of tree\n      Repository          $path      diff time   tree entries  comparisons\n      --------------------------------------------------------------------\n      git            .clang-format     0.188s          40115       18055\n      git            zlib.c            0.729s       10705983       17120\n      homebrew-core  Formula/a2ps.rb   8.390s        2122937      326758\n      homebrew-core  Formula/zzz.rb   80.495s     1235547836      326758\n\n    This is also noticeable when looking at the average number of\n    scanned tree entries and tree comparisons when running 'git\n    rev-list HEAD -- $path' for \"a lot of paths\":\n\n                                Average nr      Average      Average\n                     Average    of scanned     nr of tree   nr of diff\n                    diff time  tree entries   comparisons      calls\n      ----------------------------------------------------------------\n      jdk            0.0431s       204466         5520         3702\n      elasticsearch  0.1351s       720393        17212        13457\n      rails          0.2372s      1299221        43369        33670\n      cmssw          0.2800s      1938067        24798        23739\n      go             0.4469s      3207155        76432        42346\n      glibc          0.5313s      6555231        40442        29208\n      tensorflow     0.5735s      3914851        97227        47262\n      git            0.5911s      7480371        21789        18168\n      linux          0.6163s      4004067        79514        58145\n      cpython        0.6602s      4695754        88408        70439\n      android-base   0.7320s      4312043       122280        96376\n      rust           0.7349s      5084683        68110        29847\n      webkit         1.6580s      9395349       303105       218906\n      llvm-project   2.2607s     11773792       469801       330250\n      gecko-dev      4.0964s     42396246       415843       387162\n      gcc            6.9432s     84853083       286904       174218\n\n    Note that although we have a lot less tree comparisons in the git\n    than in the linux repository, it has almost double the amount of\n    scanned tree entries, because the git repository has some\n    frequently modified biggish directories (notably its root dir, 't'\n    or 'Documentation').  As a result the average time spent diffing\n    in the two repositories is roughly the same, although the linux\n    repository is much larger.\n\n    Or that we spend the most time diffing in the gcc repository,\n    because it has by far the most scanned tree entries, even though\n    it has less tree comparisons than any of the next three slowest\n    repositories.\n\n  - The number of path components in the pathspec, i.e. its depth in\n    the directory tree seems to be irrelevant.\n\n    When checking whether a path somewhere deep in the directory tree\n    has been modified between a commit and its parent the tree-diff\n    machinery can short-circuit, and it returns as soon as it finds\n    the first leading directory that hasn't been modified.  And more\n    often than not it can short-circuit already while comparing the\n    root trees, as all the <1.5 values in the \"Average tree\n    comparisons per diff call\" column show.\n\n                     Average   Average tree     Average      Average\n                    pathspec    comparisons    nr of diff   nr of tree\n                      depth    per diff call      calls    comparisons\n      ----------------------------------------------------------------\n      android-base    5.78         1.26          96376       122280\n      cmssw           4.28         1.04          23739        24798\n      cpython         3.72         1.25          70439        88408\n      elasticsearch   9.13         1.28          13457        17212\n      gcc             5.13         1.64         174218       286904\n      gecko-dev       6.25         1.07         387162       415843\n      git             2.30         1.19          18168        21789\n      glibc           4.17         1.38          29208        40442\n      go              4.60         1.80          42346        76432\n      jdk             8.45         1.49           3702         5520\n      linux           4.49         1.36          58145        79514\n      llvm-project    5.45         1.42         330250       469801\n      rails           5.43         1.28          33670        43369\n      rust            4.70         2.28          29847        68110\n      tensorflow      5.56         2.06          47262        97227\n      webkit          6.33         1.38         218906       303105\n\n    Note the 2.xy average tree comparisons per diff call in the rust\n    and tensorflow repositories.  In the rust repository over 98% of\n    the paths are in the 'src' directory and over 93% of commits\n    modify a file under that directory, while in the tensorflow\n    repository over 92% of paths are in the 'tensorflow' directory and\n    over 90% of commits modify a file under that directory.\n    Consequently, the tree-diff machinery can only rarely\n    short-circuit while comparing the root trees of two subsequent\n    commits, but has to dive down and compare the contents of the\n    'src' or 'tensorflow' directories most of the time as well.  I\n    suspect that we would get a similar ~2 value in the homebrew-core\n    repository as well, because over 99.7% of all commits modify the\n    'Formula' directory, which contains over 90% of paths in the\n    repository.\n\nBloom filters intro\n-------------------\n\nQuoting the (quite good) Wikipedia article, \"A Bloom filter is a\nspace-efficient probabilistic data structure [...] that is used to\ntest whether an element is a member of a set. False positive matches\nare possible, but false negatives are not – in other words, a query\nreturns either 'possibly in set' or 'definitely not in set'\".\n\nA Bloom filter is a bit array, initially all bits set to 0.  To add an\nelement to a Bloom filter, the element is hashed using 'k' independent\nhash functions, the resulting hashes are turned into 'k' array\npositions, and the bits at those positions in the filter are set to 1.\nTo query for an element, the element is hashed using the same 'k' hash\nfunctions, the resulting hashes are turned into 'k' array positions,\nand the values of the bits at those positions are checked.  If all\nthose bits are set, then the element is 'possibly in set'.  If even\none of those bits is unset, then the element is 'definitely not in\nset'.\n\nSome of the for us relevant properties of Bloom filters are:\n\n  - A Bloom filter doesn't store the elements themselves, it only sets\n    a couple of bits based on the elements' hashes.  Consequently, a\n    Bloom filter can't tell which elements it contains; it can't even\n    tell the number of elements in there.\n\n  - A bit in the Bloom filter's bit array might be set by more than\n    one element.  This is where the probabilistic nature and the\n    possibility of false positives come from: it is possible that some\n    elements in the set happen to have set all 'k' bits that would\n    indicate the presence of a particular element, even though that\n    element itself is not part of the set.\n\n  - Elements can't be removed from a Bloom filter: the 'k' bits\n    indicating the presence of an element can't simply be unset,\n    because that might unset bits that were set by other elements as\n    well.\n    There are enhanced Bloom filter variants that allow removal of\n    elements, like a counting Bloom filter, but they come with\n    additional complexity and require considerably more space.\n\n  - Bloom filters can't be resized, because turning the hashes into\n    positions in the bit array critically depends on the size of the\n    bit array (usually pos[i] = hash[i] % bit_array_size).\n\n  - Bloom filters of the same size can be bitwise OR-ed to create the\n    union of the sets of elements in the filters.\n\n  - When playing long enough with probabilities and making some\n    reasonable assumptions and approximations it can be shown (see the\n    Wikipedia article) that for a desired false positive probability\n    'p' there is an optimal number of 'k' hash functions:\n\n      k = -log₂(p)    (that's a base 2 logarithm there)\n\n    For 1% false positive probability k = 6.64, but of course there\n    can only be an integral number of hash functions, so 7 it is.\n\n    Note that this value is independent both from the size of the\n    Bloom filter and from the number of elements in there.\n\n    Inversely, when using a properly sized bit array, then the false\n    positive probability falls exponentially as 'k' increases.\n\n  - To store 'n' elements in a Bloom filter with a desired false\n    positive probability using the optimal number of 'k' bits/hash\n    functions, we need a bit array with approximate size 'm':\n\n      m ≈ n * k / ln(2) ≈ n * k * 10 / 7\n\n    When using 7 hash functions to aim for < 1% false positive\n    probability this simplifies down to:\n\n      m ≈ n * 7 * 10 / 7 = 10 * n\n\n    i.e. approx. 10 bits per element.\n\n  - In general the more elements, IOW the more set bits there are in a\n    Bloom filter of certain size, the higher the probability of false\n    positives.  Similarly, the larger the size of a Bloom filter's bit\n    array for a certain number of elements, the lower the probability\n    of false positives.\n\n  - A Bloom filter with all bits set appears to contain \"everything\",\n    because all queries will return 'possibly in set'.\n\nModified Path Bloom Filters\n---------------------------\n\nWe'll use Bloom filters to store modified paths and will store them in\nthe Modified Path Bloom Filters chunk.  Yes, plural: we'll use a lot\nof small Bloom filters, each one storing all paths modified by one\ncommit compared to one of its parents (see the Alternatives section\nnear the end for reasons for not using one big Bloom filter).  Then as\nthe pathspec-limited revision walk iterates over all commits it will\nquery each modified path Bloom filter to see whether the given\npathspec is in there, and if it is 'definitely not in set', then we\ncan spare a tree-diff call and skip the commit right away, but if it's\n'possibly in set', then we must do the tree-diff to make sure.\n\nEach modified path Bloom filter consists of:\n\n  - 4 bytes specifying the number of bits in the Bloom filter's bit\n    array.\n\n    For practical purposes 32 bit is more than sufficient to store the\n    number of bits in the Bloom filter's array.  When using k = 7\n    hashes, i.e. 10 bits per path, then we could store over 400\n    million paths in a single Bloom filter; with k = 32 hashes we'd\n    use 46 bit per path, which is still over 93 million paths.\n\n  - The actual bit array.  See the next and the Hashing scheme\n    sections for details about how this bit array is used.\n\nAll modified path Bloom filters will use the same k number of hashes\nper path, so we won't have to store that in each Bloom filter.  I\nsuspect that having modified path Bloom filters with different k\nnumber of hashes wouldn't bring enough benefit to justify storing that\nvalue in each and every filter.\n\nThe order of modified path Bloom filters in this chunk is unspecified\non purpose, so implementations can experiment with writing them in\nhistory or topological order, which may bring performance benefits\nthrough better locality.\n\n[Somewhere in the middle of this section the length of this commit\nmessage surpassed the length of 72441af7c4 (tree-diff: rework\ndiff_tree() to generate diffs for multiparent cases as well,\n2014-04-07), the mother of all commit messages from the great Kirill\nSmelkov, yay! :)]\n\nModified Path Bloom Filter Index\n--------------------------------\n\nSince modified path Bloom filters vary in size, we can't have an array\nof modified path Bloom filters indexed by the position of the commit\nOID in the OID Lookup chunk, but we'll need a level of indirection\nbetween the commit OID position and the position of the associated\nmodified path Bloom filter.\n\nSo the Modified Path Bloom Filter Index chunk will contain an array of\noffsets to quickly map commit OID positions to offsets in the Modified\nPath Bloom Filters chunk, i.e. its Nth entry contains the offset for\nthe commit whose OID is Nth in the OID Lookup chunk.\n\nSince the commit-graph file can store information about 2^31-1\ncommits, a merely 4 byte offset per commit is clearly insufficient.\nHowever, surely noone want to have lots of gigabyte-sized modified\npath Bloom filters, so a standard 8 byte file offset will be\nunderutilized.  This allows us to use a few bits from those 8 bytes\nfor special purposes.  For now we'll have two special purpose bits:\n\n  - The most significant bit indicates that the entry is not an offset\n    into the Modified Path Bloom Filters chunk, but an \"embedded\"\n    modified path Bloom filter containing all paths modified by the\n    commit compared to its first parent; see the next section.\n\n  - The second most significant bit indicates that modified path Bloom\n    filters are stored not only for the first parent of a merge\n    commit, but for all its parents, and the entry is neither an\n    offset nor an \"embedded\" modified path Bloom filter, but an array\n    index into the Modified Path Bloom Filter Merge Index chunk; see\n    the \"Merges\" section below.\n\n  - [TODO: perhaps we might want to reserve one or two more bits for\n    special purposes?]\n\nMake these offsets relative to the start of the Modified Path Bloom\nFilters chunk, so they will depend neither on the number of chunks nor\non the combined size of any other chunks that are written before the\nModified Path Bloom Filters chunk, thus allowing implementations to\ncalculate all these offsets without knowing anything about those other\nchunks.  Furthermore, if the offsets were relative to the beginning of\nthe file, then some huge chunks could make the offsets grow too large,\nand would mess up those special purpose bits (though, arguably, an\nexabyte sized commit-graph file is just too unlikely to be worried\nabout...).\n\nAn \"all bits set\" index entry can be used to indicate that there is no\nmodified path Bloom filter stored for the corresponding commit.  A\nreader implementation can either special-case such an entry, or\ninterpret it as an embedded modified path Bloom filter that replies\n'possibly in set' to all queries, the result is the same: it has to\nresort to running tree-diff for that commit.\n\nThese embedded modified path Bloom filters have implications on the\nlow-level Bloom filter format in the Modified Path Bloom Filters chunk\nas well, namely:\n\n  - Their array contains only 63 bits, not 64, i.e. not 8 full bytes.\n    Therefore, for simplicity, we'll store the size of the bit array\n    as the number of bits, not bytes.\n\n  - Assigning special purpose to the most significant bits of the\n    index entries is convenient when the entry is an offset into the\n    Modified Path Bloom Filters chunk.  OTOH, it makes indexing into\n    the Bloom filter's array awkward if we try to treat it like an\n    ordinary array, i.e. whose 0th element comes first, because we'd\n    have to account for the special purpose bits.  Therefore, we'll\n    store the bit array kind of in big endian byte order, i.e. the\n    most significant byte first and the 0th byte last.\n\nIn addition, this chunk will start with a small header storing a bit\nof metadata that applies to all modified path Bloom filters in all\nrelated chunks.  The reason for storing this header in the Modified\nPath Bloom Filter Index chunk is that only this chunk is essential to\nuse modified path Bloom filters (if all commits modify so few files\nthat their modified path Bloom filters can all be embedded into the\nindex chunk, then the Modified Path Bloom Filters chunk will contain\nnothing and thus can be omitted; e.g. the two homebrew repositories\ncome quite close to this, as shown below).\n\nThis header contains:\n\n  - One byte storing the number of 'k' hashes per path.\n\n    Using a single byte is more than sufficient: as 'k' increases the\n    false positive probability falls exponentially, and quickly\n    becomes unnecessarily low (i.e. with k = 31 even a full\n    commit-graph containing 2^31-1 commits is expected to have only a\n    single false positive).\n\n  - [TODO: What else?  We might want to have a version byte, though if\n    a format change becomes necessary, then we could simply rename the\n    chunks just as well...]\n\nEmbedded Modified Path Bloom Filters\n------------------------------------\n\nThe ideal size of a Bloom filter to store a set of 6 or 7 elements\nwith 7 hash functions is approximately 60 or 70 bits, respectively.\nSo in the space of a 8 byte Modified Path Bloom Filter Index entry we\ncould comfortably store 6 entries by using one bit to indicate that\nthe entry is not a file offset but an \"embedded\" modified path Bloom\nfilter and the remaining 63 bits as the Bloom filter's bit array.\n\nAs the table below shows, a significant portion or even the vast\nmajority of commits modify no more than 6 paths, so we can embed a lot\nof modified path Bloom filters into the Modified Path Bloom Filter\nIndex chunk.  (Also note the unusually large percentage of empty diffs\nin the android-base and cpython repositories.)\n\n        Percentage of commits modifying <=N (or =N) paths compared to their first parents\n                   0      <=1      <=2      <=3      <=4      <=5      <=6      <=7      <=8\n                          (=1)     (=2)     (=3)     (=4)     (=5)     (=6)     (=7)     (=8)\n  --------------------------------------------------------------------------------------------\n  elasticsearch  0.70%   4.00%    5.67%    8.50%   13.17%   16.55%   18.32%   21.18%   27.47%\n                        (3.30%)  (1.67%)  (2.83%)  (4.67%)  (3.37%)  (1.77%)  (2.86%)  (6.29%)\n  jdk            0.26%   3.47%   10.60%   13.99%   15.62%   19.02%   26.62%   34.30%   40.57%\n                        (3.20%)  (7.14%)  (3.39%)  (1.63%)  (3.40%)  (7.60%)  (7.68%)  (6.27%)\n  webkit         0.05%   0.07%    0.77%    2.14%    9.15%   26.66%   38.42%   47.34%   52.83%\n                        (0.02%)  (0.71%)  (1.37%)  (7.01%) (17.51%) (11.76%)  (8.92%)  (5.49%)\n  android-base  13.20%  13.62%   14.23%   18.55%   20.91%   35.18%   42.32%   50.82%   62.05%\n                        (0.42%)  (0.62%)  (4.32%)  (2.36%) (14.28%)  (7.14%)  (8.49%) (11.23%)\n  llvm-project   0.12%   0.12%    0.94%    6.45%   25.24%   46.68%   53.60%   60.97%   67.33%\n                        (0.00%)  (0.81%)  (5.51%) (18.79%) (21.44%)  (6.92%)  (7.37%)  (6.36%)\n  gecko-dev      0.14%   0.96%    1.88%   15.44%   32.42%   46.12%   54.54%   61.37%   66.65%\n                        (0.82%)  (0.92%) (13.56%) (16.98%) (13.70%)  (8.42%)  (6.82%)  (5.28%)\n  tensorflow     0.09%   1.26%    2.72%    5.00%   26.30%   42.36%   55.17%   63.11%   69.70%\n                        (1.17%)  (1.46%)  (2.28%) (21.27%) (16.07%) (12.81%)  (7.94%)  (6.59%)\n  rails          0.10%   2.09%    5.79%   16.03%   35.57%   51.47%   58.71%   65.15%   72.96%\n                        (1.99%)  (3.70%) (10.23%) (19.54%) (15.90%)  (7.24%)  (6.44%)  (7.82%)\n  rust           0.07%   2.20%    5.11%   22.81%   42.35%   52.50%   59.29%   65.33%   70.02%\n                        (2.13%)  (2.91%) (17.70%) (19.54%) (10.15%)  (6.79%)  (6.04%)  (4.69%)\n  glibc          0.02%   7.33%   14.03%   30.86%   42.22%   52.53%   61.59%   68.50%   73.68%\n                        (7.31%)  (6.70%) (16.83%) (11.36%) (10.32%)  (9.06%)  (6.91%)  (5.19%)\n  gcc            0.00%   0.24%   10.92%   26.61%   39.85%   54.97%   63.80%   69.37%   76.52%\n                        (0.24%) (10.68%) (15.69%) (13.24%) (15.13%)  (8.82%)  (5.57%)  (7.14%)\n  go             0.00%   0.96%    9.09%   19.35%   39.97%   53.16%   65.31%   72.10%   77.36%\n                        (0.95%)  (8.13%) (10.26%) (20.63%) (13.19%) (12.15%)  (6.79%)  (5.26%)\n  cmssw          0.15%   0.19%    0.20%    2.43%   45.35%   56.49%   67.58%   73.17%   77.99%\n                        (0.03%)  (0.01%)  (2.23%) (42.92%) (11.15%) (11.08%)  (5.59%)  (4.83%)\n  linux          0.01%   0.66%    3.97%   23.49%   46.15%   62.97%   72.79%   79.16%   83.57%\n                        (0.65%)  (3.30%) (19.52%) (22.66%) (16.82%)  (9.82%)  (6.37%)  (4.41%)\n  cpython        3.07%   4.97%   27.73%   59.54%   70.34%   77.48%   81.91%   86.76%   89.34%\n                        (1.91%) (22.75%) (31.82%) (10.80%)  (7.13%)  (4.43%)  (4.85%)  (2.59%)\n  git            0.11%  27.54%   55.92%   73.79%   81.90%   87.05%   90.28%   92.29%   93.82%\n                       (27.43%) (28.38%) (17.87%)  (8.11%)  (5.15%)  (3.23%)  (2.01%)  (1.53%)\n  homebrew-cask  0.40%   0.94%   95.41%   97.42%   98.11%   98.40%   98.61%   98.79%   98.93%\n                        (0.54%) (94.46%)  (2.01%)  (0.70%)  (0.29%)  (0.21%)  (0.18%)  (0.14%)\n  homebrew-core  0.01%   0.07%   98.81%   99.35%   99.56%   99.75%   99.81%   99.84%   99.86%\n                        (0.07%) (98.74%)  (0.53%)  (0.22%)  (0.19%)  (0.06%)  (0.03%)  (0.02%)\n\nThis saves space, because the Modified Path Bloom Filters chunk will\ncontain a lot less Bloom filters (albeit those would be rather small\nfilters).\n\nIt reduces the probability of false positives, because all commits\nmodifying 1-6 paths will have larger than strictly necessary modified\npath Bloom filters.\n\nFinally, it makes the Bloom filter query ever so slightly faster,\npartly because there is no redirection into the Modified Path Bloom\nFilters chunk, and partly because we can check all bit positions in a\n63 bit modified path Bloom filter using a 64 bit mask at once, instead\nof checking those bit positions one by one, and we can use the same\nmask to check all embedded Bloom filters.\n\nThe number of paths that can be stored in a 63 bit Bloom filter\ndepending on the number of hashes per path:\n\n  k     |  3   |  4   |  5  |  6  |  7  |  8  | 9 - 11 | 12 - 14 | 15 - 22 | 23 - 44\n  ------+------+------+-----+-----+-----+-----+--------+---------+---------+--------\n  paths | <=14 | <=11 | <=8 | <=7 | <=6 | <=5 |  <=4   |   <=3   |   <=2   |   <=1\n\nHashing scheme\n--------------\n\nWe need to map each modified path to 'k' independent bit positions in\nthe Bloom filters bit array.  We want the hash function and hashing\nscheme that results in the lowest false positive rate and has the\nlowest probability of \"colliding\" paths (see below), but it should\nstill be fast to compute, and should be widely available for\nalternative Git implementations.\n\nAt this point we don't care about the runtime of pathspec-limited\nrevision walks here.  Lower false positive rate inherently leads to\nlower runtime, though we are reaching diminishing returns on common\nsetups as the false positive rate gets lower and lower... and e.g. in\nthe webkit repository we'll reach an average false positive rate of\n~0.001% with only k = 7 hashes per path.  However, on unusual\nsetups/configurations accessing tree objects might be considerably\nmore expensive than accessing commit objects, especially when using\nonly commit info stored in the commit-graph.  E.g. consider a future\nwhere we can distribute commit-graphs with modified path Bloom filters\nto partial clones containing only commit objects for most of the\nhistory: any tree-diff will be really expensive, because the tree\nobjects must be fetched from the promisor.  In such a setup every\navoidable tree-diff call counts, and low false positive rate is\nking.\n\nFor now I went with 32 bit MurmurHash3 used in enhanced double hashing\nscheme with 32 bit unsigned integer arithmetic, though as I will show\nbelow it seems that this is not the best option.\n\nSo, to map each modified path to 'k' bit positions in the Bloom\nfilter's array we first need 'k' independent hashes.  In general,\nhashing a path 'k' times with the same hash function but using 'k'\ndifferent seeds produces hashes that are independent enough.  In\npractice, to reduce the overhead of hashing, especially for larger 'k'\nvalues, some variant of double hashing is often used to generate the\n'k' independent-ish hashes from the results of only two hash function\ncalls with different seeds.  In general:\n\n  h1 = h(seed1, path)\n  h2 = h(seed2, path)\n  for (i = 0; i < k; i++)\n      pos[i] = (h1 + i * h2 + f(i)) % nr_bits\n\nDepending on how the f(i) term is defined there are a few named\nvariants:\n\n  - Double hashing: f(1) = 0:\n\n      pos[i] = (h1 + i * h2) % nr_bits\n\n  - Improved double hashing: f(i) adds a simple quadratic term:\n\n      pos[i] = (h1 + i * h2 + i^2) % nr_bits\n\n  - Enhanced double hashing: f(i) adds a not-so-simple cubic term:\n\n      pos[i] = (h1 + i * h2 + (i^3 - i) / 6) % nr_bits\n\n    This cubic term is equal to the following sequence of numbers,\n    starting from i = 0:\n\n      0, 0, 1, 4, 10, 20, 35, 56, 84, 120, etc.\n\n    These are almost the same as tetrahedral numbers, except that the\n    mathematical definition starts with 1, 4, 10..., while the OEIS\n    A000292 sequence starts with 0, 1, 4, 10...\n\n    I'm puzzled by this term being 0 not only for i = 0, but for i = 1\n    as well, because this means that if (h2 % nr_bits == 0) (see\n    below), then both pos[0] = h1 and pos[1] = h1, IOW in that case\n    the path has one less bit set.  We'll take a look at whether\n    starting the sequence with a single 0 makes a difference (it does)\n    and how that affects the false positive rate (slightly increases\n    it overall), and this will be labeled \"enhanced double hashing\n    variant #1\" below.\n\n    This sequence is supposed to be just as good as a simple i^3 term,\n    but this can easily be calculated incrementally without\n    multiplication, using only addition.  We'll take a look at how a\n    simple cubic term affects the false positive rate (it increases\n    it), and this will be labeled \"enhanced double hashing variant\n    #2\" below.\n\nThese hashing schemes usually work fairly well in practice, but in\npractice Bloom filters are primarily used to check whether an element\nis part of _one_ _big_ set, and, consequently, most of the wisdom and\nexperience out there applies to big Bloom filters.\n\nWe, however, will repeatedly check whether a particular element (i.e.\npath) is part of a large number of mostly very small sets, and our use\ncase does challenge those best practices.\n\nIn our use case it is important when two paths \"collide\", i.e. when\none path in itself sets all the bit positions that (falsely) indicate\nthe presence of another path in a modified path Bloom filter, so let's\nhave a look at that.  And let's hope against hope that I don't mess up\nthe math...\n\nSo, if we have k truly independent hashes for each path, then:\n\n  (1) The probability of two paths mapping to the same k separate bit\n      positions in a Bloom filter of size nr_bits is nr_bits! /\n      ((nr_bits - k)! * k!).\n\n  (2) The probability of a path setting only a single bit position is\n      1 / nr_bits^(k-1), and the probability of a path setting one\n      particular bit position is k / nr_bits (assuming that it does\n      set k separate bits), so the probability of a path setting a bit\n      position that happens to be the only bit position set by an\n      other path is k / nr_bits^k.\n\nIn case of our 63 bit embedded modified path Bloom filters and k = 7\nthe probabilities of these two cases are about 1.81 * 10^(-9) and\n1.77 * 10^(-12), respectively.  I think that these are low enough not\nto worry about.\n\nHowever, the hashes in the various double hashing schemes are far from\nbeing independent.  When starting out with unsigned 32 bit hashes\n(like we'll do) and using 64 bit arithmetic, then there is no integer\noverflow, because neither i nor f(i) are large enough for that.  Then,\ndue to the distributivity of the modulo operation (and because i <\nnr_bits), all double hashing schemes are equivalent to:\n\n  pos[i] = (h1 % nr_bits + i * (h2 % nr_bits) + f(i) % nr_bits) % nr_bits\n\nFor the above mentioned two colliding cases this means:\n\n  (1) if (h(seed1, \"foo\") % nr_bits == h(seed1, \"bar\") % nr_bits &&\n          h(seed2, \"foo\") % nr_bits == h(seed2, \"bar\") % nr_bits)\n\n      then all double hashing variants will work as if both paths were\n      hashed to the same h1 and h2 values, and, consequently, both\n      paths will be mapped to the same bit positions.  This has the\n      probability of 1 / nr_bits^2.\n\n  (2) if (h2 % nr_bits == 0)\n\n      then the i * h2 term will basically amount to nothing and this\n      term can be simplified away, and all bit positions will depend\n      only on h1; this has the probability of 1 / nr_bits.\n\n      In case of double hashing the f(i) term is 0 as well, meaning\n      that the path maps to a single bit position.  The probability of\n      a path setting a bit position that happens to be the only bit\n      position set by an other path is k / nr_bits^2.\n\n      The quadratic or cubic f(i) terms in improved or enhanced double\n      hashing ensure that a path is mapped to multiple bit positions\n      even in this case, though those bit positions are not nearly as\n      random as one would like.\n\nIn case of 63 bit Bloom filters and k = 7, the probabilities of these\ntwo cases are 1 / 3969 and 1 / 567, respectively.\n\nWhen using 32 bit unsigned integer arithmetic, then an integer\noverflow is definitely possible, so the double hashing formula\nbecomes:\n\n  pos[i] = (h1 + i * h2 + f(i)) % 2^32 % nr_bits\n\nIf nr_bits is a power of two, then the \"% 2^32\" term can be simplified\naway, and we end up with the same formula as with 64 bit arithmetic,\nand the probabilities of cases (1) and (2) above remain the same.\n\nIf nr_bits is not a power of two, then...  well, I don't offhand know\nhow to approach that formally :)  Anyway:\n\n  (1) This type of collision seems to occur if that two-liner\n      condition above is true and for both paths there is an overflow\n      for the same values of i, which has the approximate probability\n      of 1 / ((k - 1) * nr_bits^2).\n\n  (2) This type of collision seems to occur if (h2 % nr_bits == 0) and\n      there is either no integer overflow for any values of i or if\n      there is an overflow for all values of i, which has the\n      approximate probability of k / ((k - 1) * nr_bits^2).\n\nIn case of 63 bit Bloom filters and k = 7 the probabilities of these\ntwo cases are 1 / 23814 and 1 / 3402, respectively.\n\nThere are several other colliding cases, e.g. with double hashing\nvariants its more probable that a path maps only to 2, 3, etc. bit\npositions instead of 'k' than with 'k' truly independent hashes,\nthough I haven't looked into how the probabilities work out.\n\nAnyway, all the above already shows that:\n\n  - These colliding cases are several orders of magnitude more likely\n    with any double hashing variant than with k truly independent hash\n    functions.\n\n  - When using some sort of double hashing, then these colliding cases\n    can happen with a high enough probability that we can't just\n    ignore them.\n\n  - These colliding cases can happen much more frequently with double\n    hashing than with improved or enhanced double hashing.\n\n  - Using 64 vs 32 bit arithmetic while calculating various double\n    hashing schemes makes a difference, and suggests that 32 bit\n    arithmetic has lower false positives probability.\n\n  - Bloom filters whose size is a power of two might have higher false\n    positive probability.\n\nAll this is important, because there are repositories out there that\nmodify the same path in the majority of commits, e.g.:\n\n  - homebrew-core: contains 6535 paths, and over 99.5% of commits\n    modify the 'Formula' directory and have an embedded modified path\n    Bloom filter.\n\n  - homebrew-cask: contains 7307 paths, and over 95.5% of commits\n    modify the 'Casks' directory and have an embedded modified path\n    Bloom filter.\n\n  - rust: contains 58k+ paths, and almost 93% of commits modify the\n    'src' directory, though only over 54% of commits modify that\n    directory and have an embedded modified path Bloom filter.\n\n  - tensorflow: contains 47k+ paths, and almost 91% of commits modify\n    the 'tensorflow' directory, though only about 51% of commits\n    modify that directory and have an embedded modified path Bloom\n    filter.\n\n  - go: contains 22k+ paths, and almost 84% of commits modify the\n    'src' directory, though only over 50% of commits modify that\n    directory and have an embedded modified path Bloom filter.\n\nSo e.g. if we were to look for commits modifying a path in the\nhomebrew-core repository, which happens to map to the same bit\npositions in a 63 bit Bloom filter as the 'Formula' directory, then\nBoom! we would get over 99.5% false positive rate, effectively\nrendering modified path Bloom filters useless for that particular\npath.\n\nThe table below shows the number of paths that happen to collide with\nthe repository's frequently modified directory in embedded modified\npath Bloom filters using different hash functions and hashing schemes\nwith 32 bit unsigned integer arithmetic and 64 bit arithmetic (the\nlatter in parentheses):\n\n                |    Double hashing   |      Enhanced     |      7 seeds\n                |                     |   double hashing  |\n                |  Murmur3 |  xxHash  | Murmur3 |  xxHash | Murmur3 | xxHash\n  --------------+----------+----------+---------+---------+---------+--------\n  homebrew-core |  4  (15) |  5  (17) |  0  (0) |  0  (2) |    0    |    0\n  homebrew-cask |  2  (21) |  6  (14) |  0  (2) |  1  (1) |    0    |    0\n  rust          | 18 (143) | 38 (144) |  1 (15) |  6 (24) |    0    |    0\n  tensorflow    | 20 (110) | 18 (105) |  4 (15) |  0 (12) |    0    |    0\n  go            |  9  (66) | 12  (62) |  0  (1) |  3  (8) |    0    |    0\n  --------------+----------+----------+---------+---------+---------+--------\n  all           | 53 (355) | 79 (342) |  5 (33) | 10 (47) |    0         0\n\nThe effect of embedded modified path Bloom filters on these colliding\ncases can be both beneficial and harmful:\n\n  - We use a larger than necessary Bloom filter to hold 1-6 paths,\n    which lowers the probability of these cases considerably.\n\n    This is especially important for the homebrew repositories.  E.g.\n    in homebrew-core over 98.6% of commits modify a single file in the\n    'Formula' directory, i.e. two paths in total.  To store 2 paths\n    using 7 hashes per path we would need a Bloom filter with a 20 bit\n    array, which we would round up to 24 bits to use full bytes, i.e.\n    those 98.6% of commits would have merely 24 bit Bloom filters.\n    This makes those colliding cases all the more probable: 1 / 3456\n    for case (1) and 1 / 493.7 for case (2) with 32 bit arithmetic).\n\n  - We use Bloom filters of the same size to hold 1-6 paths, so if a\n    path were to run into these cases, then more Bloom filter queries\n    would return false positives than when using appropriately sized\n    Bloom filters.\n\nNow, there is a simple trick that can, to some extent, alleviate these\ncollision issues: check all leading directories of the pathspecs, i.e.\nwhile looking for commits modifying 'dir/subdir/file', then query the\nmodified path Bloom filters not only for the full path, but for all\nits leading directories 'dir/subdir' and 'dir' as well.  This way we\nwould only get a false positive if all bit positions of all leading\ndirectories were set as well, which can significantly reduce the\nprobability of a pathspec running afoul of these colliding cases, and,\nin general, can reduce the false positive rate by an order of\nmagnitude or three as well (see later in this patch series).\n\nChecking all leading directories is not a silver bullet, though,\nbecause it can only help if the pathspec does actually have leading\ndirectories, and the deeper the pathspec in the directory tree, i.e.\nthe more leading directories it has, the lower the false positive\nrate becomes.\n\n  - So this doesn't help pathspecs in the root tree, because they\n    obviously don't have any leading directories.\n\n  - Furthermore, this doesn't help pathspecs that are immediately\n    below such a frequently modified directory, because their only\n    leading directory is modified in the majority of commits.\n\n    This means that it can't help in the homebrew repositories,\n    because 85% or 90% of their paths are directly under their\n    frequently modified directories.\n\nThe only thing that can help even in these cases is hashing the paths\nk times using k different seeds.\n\nPhew.  Let's see some actual benchmarks, shall we?\n\nThe following tables in this section show the average false positive\nrate of various hash functions and hashing schemes while listing the\nhistories of \"a lot of paths\", i.e. 'git rev-list HEAD -- $path'.  A\n'*' denotes the lowest value in each row.  All cases use k = 7 hashes\nper path, use the same basically random seeds of immense historical\nsignificance, store 6 path in embedded modified path Bloom filters,\nand check all leading directories of the given pathspec.\n\nThe table below compares the average false positive rate of 64 bit and\n32 bit unsigned integer arithmetic using double hashing and enhanced\ndouble hashing with the MurmurHash3 hash function:\n\n                    Double hashing     | Enhanced double hashing\n                   32 bit     64 bit   |    32 bit     64 bit\n  -------------------------------------+------------------------\n  android-base   0.008539%  0.015709%  | *0.004155%  0.004961%\n  cmssw          0.006022%  0.013334%  | *0.003953%  0.004109%\n  cpython        0.036840%  0.079816%  | *0.016607%  0.019330%\n  elasticsearch  0.004069%  0.005297%  |  0.003249% *0.003101%\n  gcc            0.016982%  0.034096%  | *0.008919%  0.011152%\n  gecko-dev      0.001058%  0.002691%  | *0.000725%  0.000829%\n  git            0.144256%  0.331346%  | *0.069921%  0.079405%\n  glibc          0.026480%  0.053838%  | *0.016389%  0.017902%\n  go             0.021930%  0.050348%  | *0.012616%  0.014178%\n  homebrew-cask  0.097523%  0.508175%  | *0.009096%  0.042034%\n  homebrew-core  0.120860%  0.556014%  | *0.005360%  0.026810%\n  jdk            0.006085%  0.007526%  |  0.006431% *0.005911%\n  linux          0.010908%  0.019081%  | *0.007494%  0.007896%\n  llvm-project   0.006417%  0.009327%  | *0.003913%  0.004050%\n  rails          0.024997%  0.046829%  |  0.013134% *0.012361%\n  rust           0.038579%  0.056852%  | *0.025509%  0.027068%\n  tensorflow     0.013732%  0.023307%  | *0.008243%  0.008848%\n  webkit         0.002212%  0.002950%  | *0.001007%  0.001065%\n  -------------------------------------+------------------------\n  all            0.028395%  0.110075%  | *0.006085%  0.006675%\n  w/o homebrew   0.010940%  0.021286%  | *0.005968%  0.011023%\n\nThe 64 bit unsigned arithmetic does indeed fare worse in almost every\ncase, and significantly worse in the two homebrew repositories.\n\nSo it seems that if using some form of double hashing, then 32 bit\nunsigned integer arithmetic is the way to go, even though several\nprogramming languages lack support for unsigned types (though thanks\nto the distributivity of the modulo operation, they can simply and\ncheaply implement it using 64 bit arithmetic and a '& (2^32-1)' mask).\n\nThe table below compares the average false positive rate of various\nhashing schemes using 32 bit unsigned integer arithmetic and the\nMurmurHash3 hash function:\n\n                              Enhanced   Enhanced   Enhanced\n                               double     double     double    Improved\n                               hashing    hashing    hashing    double     Double\n                   7 seeds   (original)  variant 1  variant 2   hashing    hashing\n  ---------------------------------------------------------------------------------\n  android-base    0.004214%  *0.004155%  0.004853%  0.005193%  0.008252%  0.008539%\n  cmssw          *0.003344%   0.003953%  0.003546%  0.004732%  0.004226%  0.006022%\n  cpython         0.016120%   0.016607% *0.015896%  0.020259%  0.020311%  0.036840%\n  elasticsearch  *0.003167%   0.003249% *0.003164%  0.003531%  0.003553%  0.004069%\n  gcc             0.010281%  *0.008919%  0.010359%  0.010642%  0.012166%  0.016982%\n  gecko-dev       0.000804%   0.000725% *0.000646% *0.000648%  0.000822%  0.001058%\n  git            *0.063025%   0.069921%  0.067643%  0.080744%  0.091423%  0.144256%\n  glibc          *0.016278%   0.016389%  0.018660%  0.021094%  0.025596%  0.026480%\n  go              0.013009%  *0.012616%  0.013301%  0.014316%  0.016345%  0.021930%\n  homebrew-cask  *0.007199%   0.009096%  0.009070%  0.010722%  0.016617%  0.097523%\n  homebrew-core  *0.003041%   0.005360%  0.005277%  0.005148%  0.016150%  0.120860%\n  jdk             0.005873%   0.006431% *0.005326%  0.005955%  0.006984%  0.006085%\n  linux           0.007764%  *0.007494%  0.007714%  0.008709%  0.009429%  0.010908%\n  llvm-project   *0.003367%   0.003913%  0.003730%  0.004064%  0.004809%  0.006417%\n  rails          *0.012708%   0.013134%  0.013929%  0.016316%  0.014358%  0.024997%\n  rust           *0.024245%   0.025509%  0.025045%  0.028204%  0.032491%  0.038579%\n  tensorflow     *0.007907%   0.008243%  0.008670%  0.009913%  0.010115%  0.013732%\n  webkit         *0.000999%  *0.001007%  0.001142%  0.001113%  0.001274%  0.002212%\n  ---------------------------------------------------------------------------------\n  all            *0.005646%   0.006085%  0.006226%  0.006928%  0.009264%  0.028395%\n  w/o homebrew   *0.005888%   0.005968%  0.006152%  0.006899%  0.007807%  0.010940%\n\nDouble hashing and improved double hashing have higher average false\npositive rates than enhanced double hashing; note in particular the\nsignificantly higher false positive rate with double hashing in the\ntwo homebrew repositories.  Enhanced double hashing is only slightly\nworse than 7 different seeds, at least in this particular case (i.e.\nMurmurHash3 and these specific seeds).\n\nComparing these hashing schemes using a different hash function\n(xxHash or FNV1a) shows a similar trend; for brevity I won't include\nthose tables here.\n\nThe table below compares the average false positive rate of different\n32 bit hash functions when used in enhanced double hashing scheme with\nk = 7 and 32 bit unsigned int arithmetic, or with 7 different seeds:\n\n                     Enhanced double hashing     |             7 seeds             |  7 uint32\n                  Murmur3     xxHash     FNV1a   |  Murmur3     xxHash     FNV1a   |   SHA256\n  -----------------------------------------------+---------------------------------+-----------\n  android-base   0.004155%  0.005556%  0.006113% | 0.004214% *0.004101%  0.005918% | 0.004168%\n  cmssw          0.003953%  0.003775%  0.005087% | 0.003344%  0.003677%  0.004353% |*0.003322%\n  cpython        0.016607%  0.015919%  0.021957% | 0.016120% *0.015238%  0.023649% | 0.017632%\n  elasticsearch  0.003249%  0.003589%  0.004616% | 0.003167%  0.003360%  0.003586% |*0.003027%\n  gcc           *0.008919%  0.009376%  0.010555% | 0.010281%  0.009157%  0.011882% | 0.010338%\n  gecko-dev      0.000725%  0.000721%  0.000932% | 0.000804% *0.000611%  0.000911% | 0.000737%\n  git            0.069921%  0.063449%  0.083097% |*0.063025%  0.069137%  0.087132% | 0.063763%\n  glibc          0.016389%  0.017477%  0.024321% | 0.016278% *0.016062%  0.022641% | 0.017241%\n  go             0.012616%  0.012449%  0.016728% | 0.013009% *0.011692%  0.017214% | 0.012489%\n  homebrew-cask  0.009096%  0.025104%  0.011073% | 0.007199%  0.007343%  0.009037% |*0.007050%\n  homebrew-core  0.005360%  0.007940%  0.005450% |*0.003041%  0.003832%  0.004888% | 0.003884%\n  jdk            0.006431% *0.005451%  0.007428% | 0.005873%  0.005738%  0.006654% | 0.005749%\n  linux         *0.007494%  0.008266%  0.009917% | 0.007764%  0.007723%  0.009256% | 0.007939%\n  llvm-project   0.003913%  0.003727%  0.004584% | 0.003367% *0.003247%  0.005115% | 0.003664%\n  rails          0.013134%  0.011277%  0.014103% | 0.012708% *0.010389%  0.015928% | 0.012506%\n  rust           0.025509%  0.024596%  0.032983% |*0.024245%  0.025697%  0.031593% | 0.024794%\n  tensorflow     0.008243%  0.008743%  0.011499% |*0.007907%  0.008089%  0.011922% | 0.008033%\n  webkit         0.001007%  0.001155%  0.001401% | 0.000999% *0.000962%  0.001291% | 0.001054%\n  -----------------------------------------------+---------------------------------+----------\n  all            0.006085%  0.007327%  0.007518% | 0.005646% *0.005554%  0.007591% | 0.005857%\n  w/o homebrew   0.005968%  0.005979%  0.007545% | 0.005888% *0.005660%  0.007855% | 0.006039%\n\nMurmurHash3 and xxHash are neck and neck, be it enhanced double\nhashing or 7 different seeds, at least when we ignore the unusually\nhigh false positive rate of enhanced double hashing with xxHash in the\nhomebrew-cask repository (it stumbles upon one of those colliding\ncases discussed above).\n\nFNV1a has a decidedly higher average false positive rate than any of\nthe others.\n\nI was curious to see whether using 7 unsigned integers from SHA256\noffers any benefits (being a cryptographic hash function, it should\nprovide some high quality hash values), but apparently it doesn't fare\nany better than MurmurHash3 and xxHash.  This leads me to believe that\nboth MurmurHash3 and xxHash are as good as it gets, and I would not\nexpect that any other hash function could achieve notably lower false\npositive rates.\n\nNow let's see how these hash functions and hashing schemes fare when\nwriting commit-graph files with '--reachable' and with modified path\nBloom filters from scratch at the end of this patch series.  Hash\nfunctions tend to praise themselves about how fast they can process\nhuge chunks of data, but we'll use them to hash many tiny strings...\n\n                         Total runtime of writing a commit-graph file\n                         with modified path Bloom filters from scratch\n                                    (Time spent hashing)\n\n                     Enhanced double hashing     |             7 seeds             |  7 uint32\n                  Murmur3     xxHash     FNV1a   |  Murmur3     xxHash     FNV1a   |   SHA256\n  -----------------------------------------------+---------------------------------+----------\n  android-base    40.880s    40.368s    40.569s  |  41.375s    40.557s    41.843s  |  42.706s\n                  (0.627s)   (0.484s)   (0.777s) |  (1.467s)   (0.886s)   (2.138s) |  (3.682s)\n  cmssw           25.691s    25.224s    25.645s  |  26.715s    25.998s    27.762s  |  29.527s\n                  (0.894s)   (0.650s)   (1.179s) |  (2.083s)   (1.235s)   (3.259s) |  (5.077s)\n  cpython          8.951s     8.929s     9.067s  |   9.072s     9.015s     9.008s  |   9.275s\n                  (0.057s)   (0.042s)   (0.045s) |  (0.112s)   (0.067s)   (0.102s) |  (0.324s)\n  elasticsearch   14.470s    14.320s    14.703s  |  14.983s    14.588s    15.948s  |  16.720s\n                  (0.407s)   (0.300s)   (0.622s) |  (0.991s)   (0.594s)   (1.760s) |  (2.332s)\n  gcc             36.917s    36.724s    37.251s  |  37.971s    37.449s    38.178s  |  38.332s\n                  (0.313s)   (0.230s)   (0.346s) |  (0.694s)   (0.418s)   (0.919s) |  (1.796s)\n  gecko-dev       97.729s    96.791s    97.233s  |  99.215s    97.158s   101.403s  | 105.332s\n                  (1.730s)   (1.267s)   (2.099s) |  (3.902s)   (2.344s)   (5.773s) |  (9.553s)\n  git              5.245s     5.401s     5.412s  |   5.518s     5.457s     5.474s  |   5.494s\n                  (0.022s)   (0.017s)   (0.018s) |  (0.045s)   (0.027s)   (0.040s) |  (0.124s)\n  glibc            4.146s     4.156s     4.187s  |   4.267s     4.201s     4.278s  |   4.495s\n                  (0.060s)   (0.045s)   (0.057s) |  (0.128s)   (0.079s)   (0.144s) |  (0.331s)\n  go               3.565s     3.563s     3.564s  |   3.631s     3.582s     3.607s  |   3.727s\n                  (0.040s)   (0.030s)   (0.035s) |  (0.084s)   (0.051s)   (0.084s) |  (0.221s)\n  homebrew-cask   29.818s    29.936s    30.279s  |  30.316s    30.435s    29.942s  |  29.939s\n                  (0.025s)   (0.020s)   (0.019s) |  (0.049s)   (0.032s)   (0.038s) |  (0.153s)\n  homebrew-core   55.478s    55.534s    56.248s  |  56.551s    56.100s    56.445s  |  55.641s\n                  (0.031s)   (0.024s)   (0.023s) |  (0.062s)   (0.038s)   (0.047s) |  (0.195s)\n  jdk             19.418s    19.151s    20.246s  |  21.270s    20.037s    24.073s  |  25.916s\n                  (1.260s)   (0.878s)   (2.105s) |  (3.154s)   (1.833s)   (6.133s) |  (7.732s)\n  linux          100.837s   100.130s   101.244s  | 103.775s   101.645s   103.856s  | 109.365s\n                  (2.027s)   (1.498s)   (2.030s) |  (4.319s)   (2.682s)   (5.316s) | (11.075s)\n  llvm-project    31.188s    31.392s    31.442s  |  31.895s    31.479s    31.984s  |  32.863s\n                  (0.334s)   (0.251s)   (0.345s) |  (0.724s)   (0.437s)   (0.895s) |  (1.794s)\n  rails            5.607s     5.639s     5.694s  |   5.742s     5.720s     5.865s  |   6.087s\n                  (0.084s)   (0.063s)   (0.095s) |  (0.189s)   (0.116s)   (0.252s) |  (0.467s)\n  rust            13.250s    13.206s    13.422s  |  13.701s    13.426s    13.680s  |  13.949s\n                  (0.163s)   (0.122s)   (0.169s) |  (0.359s)   (0.217s)   (0.440s) |  (0.880s)\n  tensorflow      11.808s    11.608s    11.915s  |  12.379s    11.966s    12.860s  |  13.588s\n                  (0.368s)   (0.267s)   (0.497s) |  (0.860s)   (0.517s)   (1.369s) |  (2.081s)\n  webkit          30.469s    30.735s    30.945s  |  32.005s    31.004s    32.401s  |  33.303s\n                  (0.501s)   (0.376s)   (0.733s) |  (1.212s)   (0.725s)   (2.084s) |  (3.044s)\n\nSo xxHash is indeed the fastest, even in our use case, and MurmurHash3\ncomes in second.  The time spent hashing with 7 seeds tends to be\naround twice as much as with enhanced double hashing when using the\nsame hash function.  However, the time spent hashing is only a\nfraction of the total runtime, and while its effect on total runtime\nis measurable both in best-of-five and average, it tends to be smaller\nthan run-to-run noise.\n\nAs for availability of hash functions:\n\n  - MurmurHash3 is widely available; both the reference implementation\n    and a streaming-capable ANSI C implementation is in the public\n    domain.\n\n  - xxHash is allegedly available in a variety of programming\n    languages, as shown on www.xxhash.com (supposedly, but I haven't\n    been able to load that page since months... some cloudflare host\n    error persists)\n\n  - SHA256 is widely available, and it must be part of every Git\n    implementation in the near future anyway, but it's slower than the\n    others, and, more importantly, it doesn't scale for k > 8.\n\n  - FNV1a is so simple that anyone can implement a variant that\n    incrementally computes two hashes up to the next directory\n    separator in one go in about 20 lines of code (though note that\n    the above benchmarks didn't use such an implementation).  Alas,\n    because of its higher false positive rate it's out anyway.\n\nConclusion: we should seriously consider using MurmurHash3 (or xxHash)\nand hashing each path k times with k different seeds instead of any\ndouble hashing scheme.\n\nMerges\n------\n\nIt's not clear whether it's worth computing, storing and using\nmodified path Bloom filters for all parents of merge commits.\n\n  - The number of paths modified between a merge commit and its\n    second..nth parents is in general considerably larger than between\n    any commit and its first parent.  E.g. a lot of files are modified\n    in Git's master branch while a topic cooks for a few weeks, and\n    much more in Linux when a branch started from the previous release\n    is merged near the end of the next merge window.\n\n                                 Average number of\n                                   modified paths\n                    Percentage      compared to:\n                     of merge     first    second\n                      commits     parent   parent\n      --------------------------------------------\n      android-base     73.6%       14.1    1553.6\n      cmssw            11.0%       16.1     977.4\n      cpython          11.7%        5.1     933.7\n      elasticsearch     8.4%       40.1     246.5\n      gecko-dev         3.5%       23.5     602.0\n      git              25.3%        4.0     394.8\n      homebrew-cask     9.6%        2.7      42.8\n      jdk              25.0%      184.1     408.2\n      linux             7.4%       26.1    2268.0\n      rails            22.2%        9.6     101.0\n      rust             27.0%       15.7     397.3\n      tensorflow        9.1%       39.2    1057.4\n\n    Consequently:\n\n      - The tree-diff machinery has to work that much more to gather\n        modified paths for all parents of merge commits, significantly\n        increasing the runtime of writing the commit-graph file.\n\n      - Storing modified path Bloom filters for all parents of merge\n        commits significantly increases the size of the Modified Path\n        Bloom Filters chunk, though depending on the percentage of\n        merge commits and on the size of the other chunks the relative\n        size increase of the whole commit-graph file might not be all\n        that much.\n\n      - [TODO: A few old test results suggest that pathspec-limited\n        revision walks with default history simplification using a\n        commit-graph file storing modified path Bloom filters for all\n        merge parents are a few percents slower than when storing\n        Bloom filters only for first parents.  Even fewer old test\n        results suggest that writing all Bloom filters for first\n        parents first, and then all for second..nth parents might\n        eliminate much of that runtime difference.\n        Definitely need more benchmarking.]\n\n  - During a pathspec-limited revision walk Git's default history\n    simplification only checks whether the given path was modified\n    between a merge commit and its second..nth parents when the path\n    was modified between that merge commit and its first parent.  This\n    usually happens rarely, though these second..nth parent diffs tend\n    to be more expensive than first parent diffs (because of the\n    considerably more modified paths the tree-diff machinery can't\n    short-circuit that early).  Anyway, the potential speedup is low.\n\n  - However, with '--full-history', i.e. without any history\n    simplification, all merge commits are compared to all their\n    parents, and typical additional speedups are in the range of\n    2x-3x, while in some cases over 7x or 11x can be achieved by using\n    modified path Bloom filters for all parents of merge commits.\n\nDoes it worth it?  For me personally it doesn't, but I don't know how\noften others use '--full-history' and what trade-offs they might be\nwilling to make.\n\nSo the file format described here adds _optional_ support for storing\nmodified path Bloom filters for all parents of merge commits, and the\nusers can make this decision themselves.\n\n[TODO: describe it!]\n\nDeduplication\n-------------\n\nSome commits have identical modified path Bloom filters, because they\nmodify the same set of paths (or because they modify different set of\npaths but happen to end up setting the same bit positions in the Bloom\nfilter).  By omitting duplicates from the Modified Path Bloom Filters\nchunk its size can be reduced typically by around 5-20%, and in case\nof the android-base repository by over 69%.\n\nExplicitly allow that multiple entries of the Modified Path Bloom\nFilter Index chunk can refer to the same offset in the Modified Path\nBloom Filters chunk.  This is important, because even if an\nimplementation doesn't perform this deduplication while writing the\ncommit-graph file, it must be prepared for multiple index entries\nreferring to the same offset in commit-graph file written by a\ndifferent implementation.\n\nModified Path Bloom Filter Excludes\n-----------------------------------\n\nSome repositories contain leading directories that are modified in the\ngreat majority of commits, e.g. the homebrew-core repository's\n'Formula' directory is modified in over 99.7% of commits, while\nhomebrew-cask's 'Casks' and rust's 'src' are modified in over 93% of\ncommits.  And there is 'src/main/java/com/company/division/project' in\nconvention-following Java projects...\n\nModified path Bloom filters can't speed up revision walks when the\npathspec is such a frequently modified leading directory, because\nbecause of potential false positives we'll have to run tree-diff for\nthe majority of commits anyway.  And it doesn't really make sense to\nquery the history of such a leading directory in practice, because it\nwill list the majority of commits, so one might as well look straight\nat the output of a pathspec-less 'git log'.\n\nHowever, adding those frequently modified leading directories to the\nmodified path Bloom filters requires more space and increases the\nprobability of false positives.\n\nSo the file format described here adds support for excluding specific\npaths from modified path Bloom filters by listing them in the Modified\nPath Bloom Filter Excludes chunk.\n\n[TODO: Figure out the details!]\n\nLimitations\n-----------\n\nBecause of the possibility of false positives, if a modified path\nBloom filter query returns 'possibly in set', then we have to invoke\ntree-diff to make sure that the path in question was indeed modified\nby the given commit.  Consequently, Bloom filters can't improve\nperformance that much when looking for the history of a frequently\nmodified path, because a lot of tree-diff invocations can't be\neliminated.  In the extreme case when looking for the history of a\npath modified in every commit, then using Bloom filters will only add\nextra overhead.\n\nA modified path Bloom filter doesn't store the names of modified\npaths, it only sets a couple of bits based on those paths' hashes.\nConsequently, it can only be used when looking for the history of a\nconcrete path, and:\n\n  - It can't be used with a pathspec containing wildcards like 'git\n    log -- \"*.h\"'.\n\n    However, it could still be used when the pathspec contains leading\n    directories without wildcards, e.g. 'git log --\n    \"arch/x86/include/*.h\", by limiting tree-diff only to commits\n    modifying the 'arch/x86/include/' directory.\n\n  - It can't tell which paths were modified by a given commit; for\n    that we would still have to run tree-diff for the full tree.\n\nSubmodules [TODO]\n-----------------\n\nNo modified path Bloom filters should be stored for commits modifying\nsubmodules.\n\nThis is questionable, but is necessary to produce the same output with\nand without modified path Bloom filters.  If 'dir/submod' is a gitlink\nfile, then currently 'git rev-list HEAD -- dir/submod/whatever' lists\nall commits touching 'dir/submod', even when that 'whatever' has never\nexisted.  And that 'whatever' can be basically anything, so we will\nnot find them in any of our modified path Bloom filters, therefore in\na Bloom-filter-assisted revision walk we won't list any commits.\n\nThe only way around this is to not not write any modified path Bloom\nfilters for commits modifying submodules.\n\nNote, however, that once upon a time that command wouldn't list\nanything, either, but the behavior changed with commit 74b4f7f277\n(tree-walk.c: ignore trailing slash on submodule in\ntree_entry_interesting(), 2014-01-23) to what we have now.  As\n74b4f7f277's log message only talks about handling 'dir/submod/' and\n'dir/submod' (i.e. with and without trailing slash) consistently, I\nsuspect that this behavior change with 'dir/submod/anything' is an\nunintended and undesired side effect.  However, as it involves\nsubmodules I would rather not have an opinion :)\n\nIn any case, someone with more clues about submodules should take a\ncloser look and decide whether this is a bug or not, before this\nmodified path Bloom filter thing goes much further.  If it is a bug\nindeed, then it should be fixed and the remark about submodules should\nbe removed from the modified path Bloom filter specs.  If the current\nbehavior is desired, then the remark about submodules should be\nupdated to use proper English (IMO it must be part of the spec,\nbecause this is a subtle issue that developers of other\nimplementations (JGit, libgit2, etc.) might easily overlook).\n\nThreats to validity\n-------------------\n\n  - Random paths are... random.  Picking random paths can\n    over-represent rarely modified files.  Since modified path Bloom\n    filters bring more benefits to rarely modified paths, the reported\n    speedups later in the series might be higher than what the users\n    will usually see.  (I suppose that users more often check the logs\n    of frequently modified files than of rarely modified ones.)\n\n    Though some of these random paths made me stumble upon the issue\n    with submodules discussed above, so...\n\n  - Bugs :)  It's not that hard to make subtle bugs that don't affect\n    correctness, because the probabilistic nature of Bloom filters\n    cover them up.  However, bugs like incorrectly calculating the\n    size of a Bloom filter or having an off-by-one error in the\n    filter's array handling affect the false positive rate and in turn\n    the runtime of pathspec-limited revision walks.\n\nAlternatives considered\n-----------------------\n\nHere are some alternatives that I've considered but discarded and\nideas that I haven't (yet) followed through:\n\n  - One Bloom filter to rule them all?  No.\n\n    While the first proof of concept implementation [2] demonstrated\n    that by combining hashes of modified pathnames with commit and\n    parent object ids it is possible to use a single big Bloom filter\n    and achieve notable speedups, that approach has several drawbacks:\n\n      - Poor locality: The bit positions that are set for any element\n        are supposed to be random, so we need to access random parts\n        of the big Bloom filter array for each checked commit and\n        path, which is bad for the caches.  Furthermore, we would\n        likely need to read the whole big Bloom filter array even when\n        looking for only a handful of commits, e.g. for 'git log -3 --\n        dir/file'.\n\n      - Need to grow: As the repository grows, more and more paths\n        will be added to the modified path Bloom filter, thus lowering\n        its false positive rate.  Eventually we'll reach a point where\n        we would need to increase the size of the Bloom filter, but a\n        Bloom filter can't be resized, so we'll have to create a\n        larger Bloom filter and re-fill it from scratch.  This is a\n        considerably more expensive operation than creating a\n        modified-path-Bloom-filter-less commit-graph from scratch.\n\n      - Expired commits: Unreachable commits are eventually pruned by\n        'git gc', but elements can't be removed from a Bloom filter.\n        Consequently, all the bits set for paths modified by expired\n        commits will remain set, slightly increasing the probability\n        of false positives until the Bloom filter is eventually\n        resized/rewritten.  There are enhanced Bloom filter variants\n        that allow removal of elements, like a counting Bloom filter,\n        but they come with additional complexity and require\n        considerably more space.\n\n      - Excluded commits: Sometimes we can't or just don't want to\n        record the paths modified by a particular commit, so we need a\n        way to record which commits don't have entries in the big\n        Bloom filter.\n\n      - There are plans for split commit-graph files, because\n        rewriting the whole commit-graph file in big repositories to\n        add only relatively few new commits seems to impose\n        considerable overhead.  We would need a big modified path\n        Bloom filter in each of the split commit-graph files that\n        we'll combine together when the split commit-graph files are\n        combined.  Bloom filters of the same size can be quickly\n        combined by bitwise OR-ing their arrays, but this implies that\n        the split commit-graph files holding only a few commits would\n        have to contain as large a Bloom filter as is used in the\n        shared commit-graph file holding most of the commits in the\n        repository, wasting valuable storage space.  (Though the false\n        positive rate for commits stored in the split commit-graph\n        should be spectacularly low).\n\n    Using a small Bloom filter for each commit avoids these issues,\n    though, as discussed earlier, not without introducing issues of\n    its own.\n\n  - Xor filters \"are a faster and more concise alternative to Bloom\n    filters\", but they turned out to be too large in our use case.\n\n    Each Xor filter consists of a header and an array: the header\n    contains a 64 bit seed and a 64 bit blocklength, while the array\n    contains 3 * blocklength bytes.  Consequently, the space advantage\n    of Xor filters compared to Bloom filters is only true for large\n    filters, where the header's size is negligible compared to the\n    array's size.  We, however, have a lot of very small filters, and\n    all those extra headers cause an unacceptable size increase\n    (partly because the Xor filter's header is 4 times the size of the\n    header of our modified path Bloom filters, and partly because\n    their headers make Xor filters too large to be embedded in the\n    index chunk).\n\n                     MPBF chunk      Total\n                        size       Xor Filter\n                     (no dedup)       size\n      ------------------------------------------------\n      android-base     8620198      28840220     +334%\n      cmssw           10347018      21538283     +208%\n      cpython           526381       6393731    +1214%\n      elasticsearch    4733274       9101088     +192%\n      gcc              3724544      14637474     +393%\n      gecko-dev       20591010      56887518     +276%\n      git               160718       3115827    +1938%\n      glibc             759132       3162898     +416%\n      go                458375       2676306     +584%\n      homebrew-cask      94383       5272317    +5586%\n      homebrew-core      27823       8051585   +28938%\n      jdk             13213208      15574498     +117%\n      linux           26575278      69577374     +261%\n      llvm-project     3988133      20235803     +507%\n      rails             958691       5172643     +539%\n      rust             1985782       7082949     +356%\n      tensorflow       4173570       8333062     +200%\n      webkit           5891751      15927504     +270%\n\n    https://github.com/FastFilter/xor_singleheader\n\n  - fastrange is \"a fast alternative to the modulo reduction\", but it\n    turned out to significantly increase the average false positive\n    rate when using (enhanced) double hashing.\n\n    The traditional way to fairly map an integer value to the range\n    [0,m) is the modulo reduction, i.e. 'h % m'.  Alas, integer\n    division is a costly instruction, and we have to do it a lot while\n    checking modified path Bloom filters for every commit.  Fastrange\n    proposes to use the combination of an integer multiplication and a\n    shift as: '((uint64_t)h * (uint64_t)m) >> 32', as it is supposed\n    to be faster while being just as fair.\n\n    In my benchmarks fastrange did indeed reduce the time spent\n    checking modified path Bloom filters by up to ~10% (no table about\n    this), though that's usually only a fraction of the total runtime\n    of a pathspec-limited revision walk.  Unfortunately, as the table\n    below shows, it also increased the average false positive rate by\n    about 5x when using enhanced double hashing.  I haven't yet\n    investigated why this happens.  However, fastrange is a viable\n    option when using the same hash function with 'k' different seeds,\n    as in that case it's on par with the good old % operator, and in\n    fact its false positive rate happens to be slightly lower.\n\n                           Enhanced        |\n                        double hashing     |        7 seeds\n                                           |\n                    % operator  fastrange  | % operator  fastrange\n      -------------------------------------+----------------------\n      android-base  *0.004155%  0.013987%  |  0.004214%  0.004873%\n      cmssw          0.003953%  0.006129%  | *0.003344%  0.005513%\n      cpython        0.016607%  0.033979%  | *0.016120%  0.018069%\n      elasticsearch  0.003249% *0.003149%  |  0.003167%  0.003333%\n      gcc            0.008919%  0.013098%  |  0.010281% *0.007324%\n      gecko-dev      0.000725%  0.000923%  |  0.000804% *0.000631%\n      git            0.069921%  0.106424%  | *0.063025%  0.065347%\n      glibc          0.016389%  0.025323%  | *0.016278%  0.016796%\n      go            *0.012616%  0.020914%  |  0.013009%  0.012915%\n      homebrew-cask  0.009096%  0.182016%  |  0.007199% *0.006937%\n      homebrew-core  0.005360%  0.092957%  | *0.003041%  0.004251%\n      jdk            0.006431%  0.005732%  |  0.005873% *0.005673%\n      linux          0.007494%  0.013862%  |  0.007764% *0.007127%\n      llvm-project   0.003913%  0.004829%  |  0.003367% *0.002943%\n      rails          0.013134%  0.019524%  | *0.012708%  0.012979%\n      rust           0.025509%  0.029824%  | *0.024245%  0.025174%\n      tensorflow     0.008243%  0.012485%  | *0.007907%  0.008120%\n      webkit        *0.001007%  0.001044%  | *0.000999%  0.001220%\n      -------------------------------------+----------------------\n      all            0.006085%  0.029034%  |  0.005646% *0.005579%\n      w/o homebrew   0.005968%  0.009478%  |  0.005888% *0.005662%\n\n    https://github.com/lemire/fastrange\n\n  - fastmod \"is a header file for fast 32-bit division remainders on\n    64-bit hardware\", promising faster computation when the same\n    divisor is (re)used for multiple remainder calculations.  This is\n    exactly what we are doing when checking a modified path Bloom\n    filter, as we have to map a bunch of unsigned 32 bit hash values\n    to bit positions with pos = hash % nr_bits, using the same nr_bits\n    for all hash values.\n\n    However, benchmarking has not shown conclusive improvements: while\n    fastmod slightly reduces the time spent checking modified path\n    Bloom filters in some repositories, it slightly increases in\n    others, and any effect on the runtime of the whole\n    pathspec-limited revision walk is below the noise.  Perhaps we\n    have too many \"embedded\" modified path Bloom filters that we can\n    check with a mask.  Or perhaps my CPU is too old and its integer\n    multiplication instruction is not that much faster compared to\n    integer division.\n\n    https://github.com/lemire/fastmod\n\n  - Varints.  Using 4 bytes for the size of each Bloom filter in the\n    Modified Path Bloom Filters chunk is a lot more than necessary.\n    How much space can be saved by using varints?  How much is the\n    runtime overhead of decoding those varints?  How can we ensure\n    that varint decoding doesn't read past the end of the mmapped\n    memory region?\n\n  - Path-depth-dependent number of hash functions.\n    As shown later in this patch series, we can drastically reduce the\n    average false positive rate by checking the presence of the\n    pathspec's leading directories as well.  With that change the\n    false positive rate will depend on the depth of the path in the\n    directory tree as well: the deeper the path (i.e. the more leading\n    directories can be checked), the lower the false positive rate,\n    sometimes maybe even unnecessarily low (0% is not an\n    approximation, but there was not a single false positive, though\n    the number of checked paths at those depths is quite low):\n\n                     linux.git                  webkit.git\n\n               Number of                    Number of\n      Path      checked   Average false      checked   Average false\n      depth    paths at   positive rate     paths at   positive rate\n      depth   that depth  at that depth    that depth  at that depth\n      --------------------------------------------------------------\n        1           2      0.31974050%          0       n/a\n        2          97      0.06539501%         16       0.04927891%\n        3        1052      0.01782569%        176       0.00472128%\n        4        1674      0.00396338%        586       0.00399830%\n        5        1315      0.00350913%        775       0.00089653%\n        6         548      0.00149110%       1396       0.00025000%\n        7         113      0.00057555%       1004       0.00004777%\n        8         124      0.00085726%        397       0.00001380%\n        9          34      0.00025182%        311       0.00001468%\n       10          14      0%                 250       0.00000182%\n       11           8      0%                  45       0%\n       12          19      0%                  39       0%\n       13           0      n/a                  3       0%\n       14           0      n/a                  1       0%\n       15           0      n/a                  1       0%\n      --------------------------------------------------------------\n      all        5000      0.007607%         5000       0.001012%\n\n    So we could use fewer hashes when adding deep paths to modified\n    path Bloom filters, saving same space while still achieving quite\n    low false positive rate.  Or when aiming for consistently lower\n    false positive rate, then we could use more hashes only for\n    shallower paths.\n\n[1] The repositories used in the above benchmarks are:\n\n    android-base:\n      https://android.googlesource.com/platform/frameworks/base\n    cmssw:\n      https://github.com/cms-sw/cmssw\n    cpython:\n      https://github.com/python/cpython\n    elasticsearch:\n      https://github.com/elastic/elasticsearch\n    gcc:\n      git://gcc.gnu.org/git/gcc.git\n    gecko-dev:\n      https://github.com/mozilla/gecko-dev\n    git:\n      https://git.kernel.org/pub/scm/git/git.git\n    glibc:\n      git://sourceware.org/git/glibc.git\n    go:\n      https://github.com/golang/go\n    homebrew-cask:\n      https://github.com/homebrew/homebrew-cask\n    homebrew-core:\n      https://github.com/homebrew/homebrew-core\n    jdk:\n      https://github.com/openjdk/jdk\n    linux:\n      https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git\n    llvm-project:\n      https://github.com/llvm/llvm-project.git\n    rails:\n      https://github.com/rails/rails\n    rust:\n      https://github.com/rust-lang/rust\n    tensorflow:\n      https://github.com/tensorflow/tensorflow\n    webkit:\n      git://git.webkit.org/WebKit.git\n\n[2] First PoC Bloom filter experiment:\n    https://public-inbox.org/git/20181009193445.21908-1-szeder.dev@gmail.com/\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n .../technical/commit-graph-format.txt         | 125 ++++++++++++++++++\n 1 file changed, 125 insertions(+)\n\ndiff --git a/Documentation/technical/commit-graph-format.txt b/Documentation/technical/commit-graph-format.txt\nindex efd0f99d8b..6b041f94ee 100644\n--- a/Documentation/technical/commit-graph-format.txt\n+++ b/Documentation/technical/commit-graph-format.txt\n@@ -99,6 +99,131 @@ CHUNK DATA:\n       file's OID Lookup chunk is equal to i plus the number of commits in all\n       base graphs.  If B is non-zero, this chunk must exist.\n \n+  Modified Path Bloom Filter Index (ID: {'M', 'P', 'B', 'I'}) [Optional]\n+    2 + (N * 8) bytes\n+\n+    * 1-byte number (K) of hashes per path\n+\n+    * An array of 8 byte entries, one for each N commits stored in the\n+      commit-graph, with the ith entry associated to the ith commit in the\n+      OID Lookup chunk.\n+      Each entry should be interpreted as follows:\n+\n+      - If all bits in the 8 bytes are set, then there is no modified path\n+\tBloom filter stored for this commit.\n+\n+\tThou shalt not store a modified path Bloom filter for a commit that\n+\tmodifies a submodule (gitlink file)!\n+\n+      - If the most significant bit of the first byte is set, then the\n+\tremaining 63 bits represent the bit array of an \"embedded\" Bloom\n+\tfilter containing the set of paths that were modified between the\n+\tassociated commit and its first parent, or in case of a root commit\n+\tbetween the associated commit and the empty tree.  All embedded\n+\tmodified path Bloom filters use the same hashing scheme that is\n+\tused in the Modified Path Bloom Filters chunk, see below.\n+\n+      - If the second most significant bit of the first byte is set, then\n+\tthe last four bytes form an array position into the Modified Path\n+\tBloom Filter Merge Index chunk.  The remaining bits in the first\n+\tfour bytes should be set to 0.  This can optionally be used to\n+\tstore Bloom filters for all parents of merge commits.\n+\t[TODO: Only the last four bytes?  Shouldn't that be last 62 bits?!]\n+\n+      - If the two most significant bits of the first byte are unset, then\n+\tthe entry represents an 8 byte offset pointing to a Bloom filter\n+\tin the Modified Path Bloom Filters chunk, which contains the set\n+\tof paths that were modified between the associated commit and its\n+\tfirst parent, or in case of a root commit between the associated\n+\tcommit and the empty tree.  This offset is relative to the start\n+\tof the Modified Path Bloom Filters chunk.  Multiple entries can\n+\tpoint to the same offset.\n+\n+  Modified Path Bloom Filter Merge Index (ID: {'M', 'P', 'B', 'M'}) [Optional]\n+      An array of 8 byte entries.\n+      If a merge commit's entry in the Modified Path Bloom Filter Index\n+      chunk stores an array position into this chunk, then the entry at\n+      that position is associated with that merge commit and its first\n+      parent, the next entry with that merge commit and its second parent,\n+      etc. for all parents of that merge commit.\n+\n+      Each entry should be interpreted as follows, similar to the entries\n+      in the Modified Path Bloom Filter Index chunk:\n+\n+      - If all bits in the 8 bytes are set, then there is no modified path\n+\tBloom filter stored for the associated merge commit and its\n+\tcorresponding parent.\n+\n+      - If the MSB of the first byte is set, then the remaining 63 bits\n+\trepresent the bit array of an \"embedded\" Bloom filter containing\n+\tthe set of paths that were modified between the associated merge\n+\tcommit and its corresponding parent.  All embedded modified path\n+\tBloom filters use the same hashing scheme that is used in the\n+\tModified Path Bloom Filters chunk, see below.\n+\n+      - If the most significant bit of the first byte is unset, then the\n+\tentry represents an 8 byte offset pointing to a Bloom filter in\n+\tthe Modified Path Bloom Filters chunk, which contains the set of\n+\tpaths that were modified between the associated merge commit and\n+\tits corresponding parent.  This offset is relative to the start of\n+\tthe Modified Path Bloom Filters chunk.  Multiple entries can point\n+\tto the same offset.\n+\n+      This chunk should not exist if the commit-graph file doesn't contain\n+      a Modified Path Bloom Filter Index chunk.  This chunk can be omitted\n+      if the Modified Path Bloom Filter Index doesn't contain any array\n+      indexes pointing into it.  This chunk can be omitted even when the\n+      commit-graph contains merge commits.\n+\n+  Modified Path Bloom Filters (ID: {'M', 'P', 'B', 'F'}) [Optional]\n+      A number of consecuive modified path Bloom filters of varying sizes.\n+      Each Bloom filter consists of 4 + M bytes:\n+\n+      - The first 4 bytes specify the number of m bits in the Bloom\n+\tfilter's bit array.\n+\n+      - The next M bytes hold the Bloom filter's bit array of m bits.\n+\n+      The bits in the array are indexed in network byte order, i.e. the\n+      array's 0th bit is the least significant bit of the last byte, and\n+      the (m-1)th bit is in the first byte.  If m is not a multiple of 8,\n+      then the unused leading bits should be set to 0.\n+\n+      For each path (including leading directories) modified between a\n+      commit and its parent K bit positions should be set using the\n+      following hashing scheme:\n+\n+\tfor i in [0, K)\n+\t  bit_pos[i] = (hash1 + i * hash2 + (i * i * i - i) / 6) % m\n+\n+      where hash1 and hash2 are the 32 bit MurmurHash3 hashes of the path\n+      with seeds 0xe83c5163 and 0x3b376b0c, respectively.  These bit\n+      positions should be calculated using 32 bit unsigned integer\n+      arithmetic.  The directory separator is a single '/'.  Directories\n+      should be added without a trailing '/'.\n+\n+      The order of Bloom filters in this chunk is unspecified.  Multiple\n+      entries in the Modified Path Bloom Filter Index or Modified Path\n+      Bloom Filter Merge Index chunks can point to the same Bloom filter\n+      in this chunk.\n+\n+      This chunk should not exist if the commit-graph doesn't contain a\n+      Modified Path Bloom Filter Index chunk.  This chunk can be omitted\n+      if neither the Modified Path Bloom Filter Index nor the Modified\n+      Path Bloom Filter Merge Index chunks contain any offsets pointing\n+      into it.\n+\n+  Modified Path Bloom Filter Excludes (ID: {'M', 'P', 'B', 'X'}) [Optional]\n+      A number of consecutive null terminated strings in memcmp() order.\n+      Paths in these strings should not be added to any modified path\n+      Bloom filters.\n+      [TODO: Clarify!  Only literal matches, or should we support\n+      globbing/patterns as well?  Only full match, or leading directories\n+      as well?]\n+\n+      This chunk should not exist if the commit-graph doesn't contain a\n+      Modified Path Bloom Filter Index chunk.\n+\n TRAILER:\n \n \tH-byte HASH-checksum of all of the above.\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398858","messageId":"20200529085038.26008-19-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 18/34] commit-graph: write \"empty\" Modified Path Bloom Filter Index chunk","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:22Z","receivedAt":"2020-05-29T08:51:31Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Write the Modifed Path Bloom Filter Index chunk if the\n'core.modifiedPathBloomFilters' configuration variable is set.  For\nnow store only \"no Bloom filter for this commit\" entries.\n\nThe format of the various modified path Bloom filter-related chunks\nare not (yet) compatible with split commit-graph files, so ignore the\nconfig setting and don't write modifed path Bloom filters with\n'--split'.\n---\n Documentation/config/core.txt |  6 ++++++\n commit-graph.c                | 40 +++++++++++++++++++++++++++++++++++\n 2 files changed, 46 insertions(+)\n\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex 9e440b160d..820f7d3bcf 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -588,6 +588,12 @@ core.commitGraph::\n \tto parse the graph structure of commits. Defaults to true. See\n \tlinkgit:git-commit-graph[1] for more information.\n \n+core.modifiedPathBloomFilters::\n+\tEXPERIMENTAL!!\n+\tIf true, then Git will store and use Bloom filters in the\n+\tcommit-graph file to speed up pathspec-limited revision walks.\n+\tDefaults to false.\n+\n core.useReplaceRefs::\n \tIf set to `false`, behave as if the `--no-replace-objects`\n \toption was given on the command line. See linkgit:git[1] and\ndiff --git a/commit-graph.c b/commit-graph.c\nindex e3adffd58b..28f147a418 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -21,6 +21,7 @@\n #define GRAPH_CHUNKID_DATA 0x43444154 /* \"CDAT\" */\n #define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* \"EDGE\" */\n #define GRAPH_CHUNKID_BASE 0x42415345 /* \"BASE\" */\n+#define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX 0x4d504249 /* \"MPBI\" */\n \n #define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16)\n \n@@ -33,6 +34,10 @@\n \n #define GRAPH_LAST_EDGE 0x80000000\n \n+#define GRAPH_MODIFIED_PATH_BLOOM_FILTER_NONE 0xffffffffffffffff\n+\n+#define GRAPH_MODIFIED_PATH_BLOOM_FILTER_DEFAULT_NR_HASHES 7\n+\n #define GRAPH_HEADER_SIZE 8\n #define GRAPH_FANOUT_SIZE (4 * 256)\n #define GRAPH_CHUNKLOOKUP_WIDTH 12\n@@ -791,6 +796,11 @@ struct write_commit_graph_context {\n \t\t check_oids:1;\n \n \tconst struct split_commit_graph_opts *split_opts;\n+\n+\tstruct modified_path_bloom_filter_context {\n+\t\tunsigned use_modified_path_bloom_filters:1;\n+\t\tunsigned int num_hashes;\n+\t} mpbfctx;\n };\n \n static int write_graph_chunk_fanout(struct hashfile *f,\n@@ -990,6 +1000,20 @@ static int write_graph_chunk_extra_edges(struct hashfile *f,\n \treturn 0;\n }\n \n+static int write_graph_chunk_modified_path_bloom_index(struct hashfile *f,\n+\t\tstruct write_commit_graph_context *ctx)\n+{\n+\tconst uint64_t no_bloom_filter = GRAPH_MODIFIED_PATH_BLOOM_FILTER_NONE;\n+\tint i;\n+\n+\thashwrite(f, &ctx->mpbfctx.num_hashes, sizeof(uint8_t));\n+\tfor (i = 0; i < ctx->commits.nr; i++) {\n+\t\tdisplay_progress(ctx->progress, ++ctx->progress_cnt);\n+\t\thashwrite(f, &no_bloom_filter, sizeof(no_bloom_filter));\n+\t}\n+\treturn 0;\n+}\n+\n static int oid_compare(const void *_a, const void *_b)\n {\n \tconst struct object_id *a = (const struct object_id *)_a;\n@@ -1428,6 +1452,14 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tchunks[chunks_nr].write_fn = write_graph_chunk_extra_edges;\n \t\tchunks_nr++;\n \t}\n+\tif (ctx->mpbfctx.use_modified_path_bloom_filters) {\n+\t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\t\tchunks[chunks_nr].id = GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX;\n+\t\tchunks[chunks_nr].size = sizeof(uint8_t) +\n+\t\t\t\t\t ctx->commits.nr * sizeof(uint64_t);\n+\t\tchunks[chunks_nr].write_fn = write_graph_chunk_modified_path_bloom_index;\n+\t\tchunks_nr++;\n+\t}\n \tif (ctx->num_commit_graphs_after > 1) {\n \t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \t\tchunks[chunks_nr].id = GRAPH_CHUNKID_BASE;\n@@ -1824,6 +1856,14 @@ int write_commit_graph(const char *obj_dir,\n \tctx->check_oids = flags & COMMIT_GRAPH_WRITE_CHECK_OIDS ? 1 : 0;\n \tctx->split_opts = split_opts;\n \n+\tgit_config_get_bool(\"core.modifiedPathBloomFilters\", &res);\n+\tif (res && ctx->split) {\n+\t\twarning(\"not writing modified path Bloom filters with --split\");\n+\t} else if (res) {\n+\t\tctx->mpbfctx.use_modified_path_bloom_filters = 1;\n+\t\tctx->mpbfctx.num_hashes = GRAPH_MODIFIED_PATH_BLOOM_FILTER_DEFAULT_NR_HASHES;\n+\t}\n+\n \tif (ctx->split) {\n \t\tstruct commit_graph *g;\n \t\tprepare_commit_graph(ctx->r);\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398859","messageId":"20200529085038.26008-20-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 19/34] commit-graph: add commit slab for modified path Bloom filters","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:23Z","receivedAt":"2020-05-29T08:51:34Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"---\n commit-graph.c | 36 +++++++++++++++++++++++++++++++++++-\n 1 file changed, 35 insertions(+), 1 deletion(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 28f147a418..3a38f25ce9 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -14,6 +14,8 @@\n #include \"hashmap.h\"\n #include \"replace-object.h\"\n #include \"progress.h\"\n+#include \"bloom-filter.h\"\n+#include \"commit-slab.h\"\n \n #define GRAPH_SIGNATURE 0x43475048 /* \"CGPH\" */\n #define GRAPH_CHUNKID_OIDFANOUT 0x4f494446 /* \"OIDF\" */\n@@ -47,6 +49,20 @@\n /* Remember to update object flag allocation in object.h */\n #define REACHABLE       (1u<<15)\n \n+struct modified_path_bloom_filter_info {\n+\tstruct bloom_filter filter;\n+};\n+\n+static void free_modified_path_bloom_filter_info_in_slab(\n+\t\tstruct modified_path_bloom_filter_info *bfi)\n+{\n+\tbloom_filter_free(&bfi->filter);\n+}\n+\n+define_commit_slab(modified_path_bloom_filters,\n+\t\t   struct modified_path_bloom_filter_info);\n+static struct modified_path_bloom_filters modified_path_bloom_filters;\n+\n char *get_commit_graph_filename(const char *obj_dir)\n {\n \tchar *filename = xstrfmt(\"%s/info/commit-graph\", obj_dir);\n@@ -1008,8 +1024,18 @@ static int write_graph_chunk_modified_path_bloom_index(struct hashfile *f,\n \n \thashwrite(f, &ctx->mpbfctx.num_hashes, sizeof(uint8_t));\n \tfor (i = 0; i < ctx->commits.nr; i++) {\n+\t\tstruct commit *commit = ctx->commits.list[i];\n+\t\tstruct modified_path_bloom_filter_info *bfi;\n+\n \t\tdisplay_progress(ctx->progress, ++ctx->progress_cnt);\n-\t\thashwrite(f, &no_bloom_filter, sizeof(no_bloom_filter));\n+\n+\t\tbfi = modified_path_bloom_filters_peek(\n+\t\t\t\t&modified_path_bloom_filters, commit);\n+\n+\t\tif (!bfi || !bfi->filter.nr_bits)\n+\t\t\thashwrite(f, &no_bloom_filter, sizeof(no_bloom_filter));\n+\t\telse\n+\t\t\tBUG(\"writing proper Bloom filters is not implemented yet\");\n \t}\n \treturn 0;\n }\n@@ -1864,6 +1890,9 @@ int write_commit_graph(const char *obj_dir,\n \t\tctx->mpbfctx.num_hashes = GRAPH_MODIFIED_PATH_BLOOM_FILTER_DEFAULT_NR_HASHES;\n \t}\n \n+\t\tinit_modified_path_bloom_filters(&modified_path_bloom_filters);\n+\t}\n+\n \tif (ctx->split) {\n \t\tstruct commit_graph *g;\n \t\tprepare_commit_graph(ctx->r);\n@@ -1984,6 +2013,11 @@ int write_commit_graph(const char *obj_dir,\n \t\tfree(ctx->commit_graph_hash_after);\n \t}\n \n+\tif (ctx->mpbfctx.use_modified_path_bloom_filters)\n+\t\tdeep_clear_modified_path_bloom_filters(\n+\t\t\t\t&modified_path_bloom_filters,\n+\t\t\t\tfree_modified_path_bloom_filter_info_in_slab);\n+\n \tfree(ctx);\n \n \treturn res;\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398861","messageId":"20200529085038.26008-21-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 20/34] commit-graph: fill the Modified Path Bloom Filter Index chunk","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:24Z","receivedAt":"2020-05-29T08:51:35Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Collect all paths modified by all commits, and write embedded modified\npath Bloom filters for those commits that modify few enough paths to\nfit into a 63 bit array using 7 hashes per path, i.e. 6 or less.\n\nFor now all modified path Bloom filters are created from scratch,\nre-using filters from the existing commit-graph file will be done in a\nlater patch in this series.\n\nPlug this into the custom revision walking loop in close_reachable(),\nso the iteration is at least somewhat friendly to the delta base\ncache, and we won't have yet another loop iterating over all commits.\nTake care care that this loop might encounter the same commits\nmultiple times (e.g. because more than one refs point to the same\ncommit, or a commit is present in more than one pack file), and skip\ncreating a modified path Bloom filter for already processed commits.\nWe wouldn't need to be careful about this if we were to create\nmodified path Bloom filters a bit later on, after the list of commits\nhave been deduplicated, but at that point the commits are in SHA-1\norder, i.e. effectively random, which is downright hostile to the\ncaches.\n\nPerhaps processing the modified paths returned by the tree-diff\nmachinery turned out to be more complicated than strictly necessary,\nbut...\n\nSo, my initial approach was to first let diff_tree_oid() and\ndiffcore_std() do their thing and put all modified files into a diff\nqueue, then iterate over that queue and add all paths (i.e. leading\ndirectories as well) to a hashmap with a suitable comparison function\nfor deduplication, use the number of elements in that hashmap to\nproperly size the Bloom filter's bit array, and finally iterate over\nall paths in the hashmap and add them to the Bloom filter.  Simple,\nshort, straightforward, almost idiomatic even, without subtle corner\ncases.\n\nThis approach, however, involved lot of memory allocations: three\nallocations for each entry in the diff queue (IOW for each modified\nfile), and one allocation for each hashmap entry (IOW for each\nmodified path), not counting allocations for the hashmap's internals.\nAll these memory allocations caused considerable overhead.\n\nThis patch implements a more efficient, but more complex, approach:\nregister callback functions in 'struct diff_options' for add/removal\nand modification, which process, deduplicate, and hash modified paths\n_while_ the tree-diff machinery scans and compares the two trees.\nTree-diff calls these callback functions with the filenames in order\n[1], so the deduplication of modified paths basically boils down to\nskipping the common prefix with the previously processed path (and\nsome subtlety in case of dir-file replacement, as explained in an\nin-code comment).  This way we don't do any memory allocations for\neach modified file or path while processing the diff output, and the\nmemory allocations that are still left are amortized over the whole\ncommit-graph writing process.\n\n[1] The tree-diff machinery would call its callbacks with out of order\n    filenames if it encountered a bogus tree object with unsorted\n    entries.  This doesn't affect the correctness of modified\n    path Bloom filters only their sizes: the worst thing that can\n    happen is that this approach would count and hash some paths more\n    than once, thus Bloom filters containing paths modified compared\n    to such a bogus tree object would end up bigger than expected.\n    And bogus unsorted tree object cause all kinds of weird diff\n    issues anyway...\n\n[TODO: The pathchange callback is no good.]\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 243 ++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 238 insertions(+), 5 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 3a38f25ce9..413605a29c 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -16,6 +16,8 @@\n #include \"progress.h\"\n #include \"bloom-filter.h\"\n #include \"commit-slab.h\"\n+#include \"diff.h\"\n+#include \"compat/PMurHash.h\"\n \n #define GRAPH_SIGNATURE 0x43475048 /* \"CGPH\" */\n #define GRAPH_CHUNKID_OIDFANOUT 0x4f494446 /* \"OIDF\" */\n@@ -37,8 +39,10 @@\n #define GRAPH_LAST_EDGE 0x80000000\n \n #define GRAPH_MODIFIED_PATH_BLOOM_FILTER_NONE 0xffffffffffffffff\n+#define GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS 63\n \n #define GRAPH_MODIFIED_PATH_BLOOM_FILTER_DEFAULT_NR_HASHES 7\n+#define GRAPH_MODIFIED_PATH_BLOOM_FILTER_DEFAULT_EMBEDDED_LIMIT 6\n \n #define GRAPH_HEADER_SIZE 8\n #define GRAPH_FANOUT_SIZE (4 * 256)\n@@ -773,6 +777,36 @@ struct tree *get_commit_tree_in_graph(struct repository *r, const struct commit\n \treturn get_commit_tree_in_graph_one(r, r->objects->commit_graph, c);\n }\n \n+static uint32_t modified_path_bloom_seeds[] = {\n+\t0xe83c5163U,\n+\t0x3b376b0cU,\n+\t0x72441af7U,\n+\t0x433860f3U,\n+\t0x6d9617f4U,\n+\t0x22705334U,\n+\t0xc8af66abU,\n+\t0x43b44ccfU,\n+\t0xb84f767cU,\n+\t0x1da177e4U\n+};\n+\n+static void compute_modified_path_bloom_hashes_for_path(const char *path,\n+\t\tsize_t len, unsigned int num_hashes, uint32_t *hashes)\n+{\n+\tuint32_t h1, h2, i;\n+\n+\th1 = PMurHash32(modified_path_bloom_seeds[0], path, len);\n+\th2 = PMurHash32(modified_path_bloom_seeds[1], path, len);\n+\n+\t/* Equivalent to hashes[i] = h1 + i * h2 + (i * i * i - i) / 6 */\n+\thashes[0] = h1;\n+\tfor (i = 1; i < num_hashes; i++) {\n+\t\th1 += h2;\n+\t\th2 += i;\n+\t\thashes[i] = h1;\n+\t}\n+}\n+\n struct packed_commit_list {\n \tstruct commit **list;\n \tint nr;\n@@ -816,6 +850,29 @@ struct write_commit_graph_context {\n \tstruct modified_path_bloom_filter_context {\n \t\tunsigned use_modified_path_bloom_filters:1;\n \t\tunsigned int num_hashes;\n+\t\t/*\n+\t\t * Number of paths to be added to \"embedded\" modified path\n+\t\t * Bloom filters.\n+\t\t */\n+\t\tint embedded_limit;\n+\n+\t\tstruct diff_options diffopt;\n+\n+\t\t/*\n+\t\t * Used to deduplicate paths modified by the currenly\n+\t\t * processed commit.\n+\t\t */\n+\t\tstruct strbuf prev_path;\n+\t\tsize_t *hashed_prefix_lengths;\n+\t\tint hashed_prefix_lengths_nr, hashed_prefix_lengths_alloc;\n+\n+\t\t/*\n+\t\t * The tree-diff callbacks will fill this array with\n+\t\t * 'num_hashes' hash values for each path modified by the\n+\t\t * currently processed commit.\n+\t\t */\n+\t\tuint32_t *hashes;\n+\t\tint hashes_nr, hashes_alloc;\n \t} mpbfctx;\n };\n \n@@ -1032,10 +1089,20 @@ static int write_graph_chunk_modified_path_bloom_index(struct hashfile *f,\n \t\tbfi = modified_path_bloom_filters_peek(\n \t\t\t\t&modified_path_bloom_filters, commit);\n \n-\t\tif (!bfi || !bfi->filter.nr_bits)\n+\t\tif (!bfi || !bfi->filter.nr_bits) {\n \t\t\thashwrite(f, &no_bloom_filter, sizeof(no_bloom_filter));\n-\t\telse\n-\t\t\tBUG(\"writing proper Bloom filters is not implemented yet\");\n+\t\t} else if (bfi->filter.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS) {\n+\t\t\tuint8_t filterdata[sizeof(uint64_t)];\n+\t\t\tmemcpy(filterdata, bfi->filter.bits,\n+\t\t\t       sizeof(filterdata));\n+\t\t\t/*\n+\t\t\t * Set the most significant bit of the first byte to\n+\t\t\t * indicate embedded modified path Bloom Filter.\n+\t\t\t */\n+\t\t\tfilterdata[0] |= 1 << 7;\n+\t\t\thashwrite(f, filterdata, sizeof(filterdata));\n+\t\t} else\n+\t\t\tBUG(\"writing non-embedded Bloom filters is not implemented yet\");\n \t}\n \treturn 0;\n }\n@@ -1074,9 +1141,162 @@ static int add_packed_commits(const struct object_id *oid,\n \treturn 0;\n }\n \n+static void handle_modified_file(\n+\t\tstruct modified_path_bloom_filter_context *mpbfctx,\n+\t\tconst char *path)\n+{\n+\tconst char *prev = mpbfctx->prev_path.buf;\n+\tconst char *p = path;\n+\tsize_t common_prefix_len, already_hashed_prefix_len = 0;\n+\tint i;\n+\n+\t/*\n+\t * We want to hash each modified path only once, i.e. if a commit\n+\t * modifies both 'dir/foo' and 'dir/bar', then we want to hash\n+\t * 'dir' only once.\n+\t */\n+\n+\t/*\n+\t * Skip common prefix with the previously handled path, so we\n+\t * hash any common leading directories only once.\n+\t */\n+\twhile (*prev && *p && *prev == *p) {\n+\t\tprev++;\n+\t\tp++;\n+\t}\n+\tcommon_prefix_len = p - path;\n+\n+\t/*\n+\t * However, merely skipping the common prefix is not enough.\n+\t *\n+\t * If a commit removes a file (symlink, or submodule) and adds a\n+\t * directory with the same name (or vice versa), e.g. removes\n+\t * 'foo' and adds 'foo/bar', and adds/modifies/removes 'foo.c',\n+\t * then the tree-diff machinery will call this function with\n+\t * those paths in the following order:\n+\t *\n+\t *   foo\n+\t *   foo.c\n+\t *   foo/bar\n+\t *\n+\t * So the first call hashes 'foo', the second hashes 'foo.c', so\n+\t * far so good.  In the third call the common prefix of 'foo.c'\n+\t * and 'foo/bar' is 'foo', and then an immediate strchrnul(p, '/')\n+\t * would find the subsequent '/' and would hash 'foo' _again_.\n+\t * Not so good.\n+\t *\n+\t * To avoid this we maintain the length of already hashed common\n+\t * prefixes and skip one more character (i.e. the '/') if\n+\t * necessary.\n+\t */\n+\tfor (i = 0; i < mpbfctx->hashed_prefix_lengths_nr; i++) {\n+\t\tif (common_prefix_len < mpbfctx->hashed_prefix_lengths[i]) {\n+\t\t\tmpbfctx->hashed_prefix_lengths_nr = i;\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\talready_hashed_prefix_len = mpbfctx->hashed_prefix_lengths[i];\n+\t}\n+\tif (common_prefix_len == already_hashed_prefix_len)\n+\t\tp = path + already_hashed_prefix_len + 1;\n+\n+\tdo {\n+\t\tunsigned int new_hashes_nr = mpbfctx->hashes_nr + mpbfctx->num_hashes;\n+\n+\t\tp = strchrnul(p, '/');\n+\n+\t\tALLOC_GROW(mpbfctx->hashes, new_hashes_nr,\n+\t\t\t   mpbfctx->hashes_alloc);\n+\t\tcompute_modified_path_bloom_hashes_for_path(path, p - path,\n+\t\t\t\tmpbfctx->num_hashes,\n+\t\t\t\tmpbfctx->hashes + mpbfctx->hashes_nr);\n+\t\tmpbfctx->hashes_nr = new_hashes_nr;\n+\n+\t\tALLOC_GROW(mpbfctx->hashed_prefix_lengths,\n+\t\t\t   mpbfctx->hashed_prefix_lengths_nr + 1,\n+\t\t\t   mpbfctx->hashed_prefix_lengths_alloc);\n+\t\tmpbfctx->hashed_prefix_lengths[mpbfctx->hashed_prefix_lengths_nr] = p - path;\n+\t\tmpbfctx->hashed_prefix_lengths_nr++;\n+\t} while (*p && p++);\n+\n+\t/*\n+\t * Update the previous path for the next iteration, copying only\n+\t * as many characters as necessary.\n+\t */\n+\tstrbuf_setlen(&mpbfctx->prev_path, common_prefix_len);\n+\tstrbuf_add(&mpbfctx->prev_path, path + common_prefix_len,\n+\t\t   p - (path + common_prefix_len));\n+}\n+\n+static void file_add_remove_cb(struct diff_options *options,\n+\t\t\t       int addremove, unsigned mode,\n+\t\t\t       const struct object_id *oid,\n+\t\t\t       int oid_valid, const char *fullpath,\n+\t\t\t       unsigned dirty_submodule)\n+{\n+\thandle_modified_file(options->change_fn_data, fullpath);\n+}\n+\n+static void file_change_cb(struct diff_options *options,\n+\t\t\t   unsigned old_mode, unsigned new_mode,\n+\t\t\t   const struct object_id *old_oid,\n+\t\t\t   const struct object_id *new_oid,\n+\t\t\t   int old_oid_valid, int new_oid_valid,\n+\t\t\t   const char *fullpath,\n+\t\t\t   unsigned old_dirty_submodule,\n+\t\t\t   unsigned new_dirty_submodule)\n+{\n+\thandle_modified_file(options->change_fn_data, fullpath);\n+}\n+\n+static void create_modified_path_bloom_filter(\n+\t\tstruct modified_path_bloom_filter_context *mpbfctx,\n+\t\tstruct commit *commit)\n+{\n+\tstruct modified_path_bloom_filter_info *bfi;\n+\tstruct object_id *parent_oid;\n+\tuintmax_t path_component_count;\n+\n+\tif (!mpbfctx->use_modified_path_bloom_filters)\n+\t\treturn;\n+\n+\t/*\n+\t * This function can be called multiple times for the same commit\n+\t * (a commit can be present in multiple pack files, or multiple\n+\t * refs can point to the same commit) so check first whether we have\n+\t * already created a modified path Bloom filter for it.\n+\t */\n+\tbfi = modified_path_bloom_filters_peek(&modified_path_bloom_filters,\n+\t\t\t\t\t       commit);\n+\tif (bfi && bfi->filter.nr_bits)\n+\t\treturn;\n+\n+\tparent_oid = commit->parents ? &commit->parents->item->object.oid :\n+\t\t\t\t       NULL;\n+\tmpbfctx->hashes_nr = 0;\n+\tstrbuf_reset(&mpbfctx->prev_path);\n+\tdiff_tree_oid(parent_oid, &commit->object.oid, \"\", &mpbfctx->diffopt);\n+\tpath_component_count = mpbfctx->hashes_nr / mpbfctx->num_hashes;\n+\n+\tif (path_component_count > mpbfctx->embedded_limit) {\n+\t\t/* Not implemented yet. */\n+\t} else {\n+\t\tbfi = modified_path_bloom_filters_at(\n+\t\t\t\t&modified_path_bloom_filters, commit);\n+\n+\t\tbloom_filter_init_with_size(&bfi->filter,\n+\t\t\t\tGRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS);\n+\t\tbloom_filter_set_bits(&bfi->filter, mpbfctx->hashes,\n+\t\t\t\t      mpbfctx->hashes_nr);\n+\t}\n+}\n+\n static void add_missing_parents(struct write_commit_graph_context *ctx, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\n+\tcreate_modified_path_bloom_filter(&ctx->mpbfctx, commit);\n+\n \tfor (parent = commit->parents; parent; parent = parent->next) {\n \t\tif (!(parent->item->object.flags & REACHABLE)) {\n \t\t\tALLOC_GROW(ctx->oids.list, ctx->oids.nr + 1, ctx->oids.alloc);\n@@ -1888,9 +2108,18 @@ int write_commit_graph(const char *obj_dir,\n \t} else if (res) {\n \t\tctx->mpbfctx.use_modified_path_bloom_filters = 1;\n \t\tctx->mpbfctx.num_hashes = GRAPH_MODIFIED_PATH_BLOOM_FILTER_DEFAULT_NR_HASHES;\n-\t}\n+\t\tctx->mpbfctx.embedded_limit = GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS / (ctx->mpbfctx.num_hashes * 10 / 7);\n \n \t\tinit_modified_path_bloom_filters(&modified_path_bloom_filters);\n+\n+\t\trepo_diff_setup(ctx->r, &ctx->mpbfctx.diffopt);\n+\t\tctx->mpbfctx.diffopt.flags.recursive = 1;\n+\t\tctx->mpbfctx.diffopt.add_remove = file_add_remove_cb;\n+\t\tctx->mpbfctx.diffopt.change = file_change_cb;\n+\t\tctx->mpbfctx.diffopt.change_fn_data = &ctx->mpbfctx;\n+\t\tdiff_setup_done(&ctx->mpbfctx.diffopt);\n+\n+\t\tstrbuf_init(&ctx->mpbfctx.prev_path, PATH_MAX);\n \t}\n \n \tif (ctx->split) {\n@@ -2013,10 +2242,14 @@ int write_commit_graph(const char *obj_dir,\n \t\tfree(ctx->commit_graph_hash_after);\n \t}\n \n-\tif (ctx->mpbfctx.use_modified_path_bloom_filters)\n+\tif (ctx->mpbfctx.use_modified_path_bloom_filters) {\n \t\tdeep_clear_modified_path_bloom_filters(\n \t\t\t\t&modified_path_bloom_filters,\n \t\t\t\tfree_modified_path_bloom_filter_info_in_slab);\n+\t\tstrbuf_release(&ctx->mpbfctx.prev_path);\n+\t\tfree(ctx->mpbfctx.hashed_prefix_lengths);\n+\t\tfree(ctx->mpbfctx.hashes);\n+\t}\n \n \tfree(ctx);\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398862","messageId":"20200529085038.26008-22-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 21/34] commit-graph: load and use the Modified Path Bloom Filter Index chunk","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:25Z","receivedAt":"2020-05-29T08:51:44Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Load the Modified Path Bloom Filter Index (MPBI) chunk and check the\nembedded modified path Bloom filters within to speed up\npathspec-limited revison walks.\n\nExtend 'struct pathspec_item' to hold the hashes of each path for\nmodified path Bloom filters.  Pathspecs are used by many Git commands,\nand a lot of them doesn't do any revision walk at all, so don't\ninitialize those fields in parse_pathspec(), but only when we are\nabout to start a pathspec-limited revision walk, i.e. in\nprepare_revision_walk().  Initialize these fields only in the\nrevs->pruning.pathspec instance, because that's the one used to limit\nthe revision walk.  Note that some revision walks re-parse the\npathspecs, notably 'git log --follow' and the line-level log, but they\ndon't use this pathspec instance.\n\nThe table below shows the average runtime and speedup of\n\n  git rev-list HEAD -- \"$path\"\n\nfor 5000+ randomly selected paths from each repository with and\nwithout modified path Bloom filters:\n\n                   Average runtime\n                  without      with\n                    MPBI       MPBI\n  --------------------------------------------\n  android-base    0.8780s      n/a\n  cmssw           0.3143s    0.2104s    -33.1%\n  cpython         0.7453s    0.3410s    -54.2%\n  gcc             7.1852s    3.3633s    -53.2%\n  gecko-dev       4.6113s      n/a\n  git             0.6180s    0.2184s    -64.7%\n  glibc           0.5618s    0.3245s    -42.2%\n  jdk             0.0482s    0.0395s    -18.0%\n  linux           0.7043s    0.4810s    -31.6%\n  llvm-project    2.6844s      n/a\n  rails           0.2784s    0.1792s    -35.6%\n  rust            0.7757s    0.6073s    -21.7%\n  webkit          1.9137s    1.4832s    -22.5%\n\nThe results in the homebrew-core repository can be spectacular, though\nthat repository looks like as if it was specifically designed to show\noff the benefits of this patch: over 98% of its almost 164k commits in\nits linear history modify one file within the 'Formula' directory, and\nthat directory contains almost 5000 files.  Consequently, almost all\ncommits will have a 63 bit \"embedded\" modified path Bloom filter\ncontaining only 2 paths, resulting in a false positive rate less than\n0.1%, and they can spare a lot of expensive tree entry scanning to the\nend of that directory.  So looking at the history of the first and\nlast files in that directory:\n\n                         Runtime                       Max RSS\n                   without     with              without      with\n                     MPBI      MPBI   Speedup      MPBI       MPBI\n  -----------------------------------------------------------------\n  Formula/a2ps.rb   8.390s    0.218s    38.5x    422884k    129212k\n  Formula/zzz.rb   80.495s    0.176s   450.4x    423744k     68364k\n\n('Formula/a2ps.rb' is modified 32 times and has two false positives,\nwhile 'zzz.rb' is modified only twice and happens to have only a\nsingle false positive; that's why querying the history of the former\nis slower, even though it's at the beginning of the directory).\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 146 +++++++++++++++++++++++++++++++++++++++++++++++++\n commit-graph.h |  13 +++++\n pathspec.c     |   9 +++\n pathspec.h     |  12 ++++\n revision.c     |  18 ++++++\n 5 files changed, 198 insertions(+)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 413605a29c..fb24600bb3 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -26,6 +26,7 @@\n #define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* \"EDGE\" */\n #define GRAPH_CHUNKID_BASE 0x42415345 /* \"BASE\" */\n #define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX 0x4d504249 /* \"MPBI\" */\n+#define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_EXCLUDES 0x4d504258 /* \"MPBX\" */\n \n #define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16)\n \n@@ -308,6 +309,23 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\t\t\tchunk_repeated = 1;\n \t\t\telse\n \t\t\t\tgraph->chunk_base_graphs = data + chunk_offset;\n+\t\t\tbreak;\n+\n+\t\tcase GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX:\n+\t\t\tif (graph->chunk_mpbf_index)\n+\t\t\t\tchunk_repeated = 1;\n+\t\t\telse {\n+\t\t\t\tgraph->chunk_mpbf_index = data + chunk_offset;\n+\t\t\t\tgraph->chunk_mpbf_index_size = next_chunk_offset - chunk_offset;\n+\t\t\t}\n+\t\t\tbreak;\n+\n+\t\tcase GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_EXCLUDES:\n+\t\t\tif (graph->chunk_mpbf_excludes)\n+\t\t\t\tchunk_repeated = 1;\n+\t\t\telse\n+\t\t\t\tgraph->chunk_mpbf_excludes = data + chunk_offset;\n+\t\t\tbreak;\n \t\t}\n \n \t\tif (chunk_repeated) {\n@@ -324,6 +342,29 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\treturn NULL;\n \t}\n \n+\tif (graph->chunk_mpbf_index) {\n+\t\tint res = 0;\n+\t\tgit_config_get_bool(\"core.modifiedPathBloomFilters\", &res);\n+\t\tif (res) {\n+\t\t\tuint64_t expected_size = sizeof(uint8_t) +\n+\t\t\t\t\t\t graph->num_commits * sizeof(uint64_t);\n+\t\t\tif (graph->chunk_mpbf_index_size != expected_size)\n+\t\t\t\tBUG(_(\"for %u commits the Modified Path Bloom Filter Index chunk should be %ld bytes, but it's %ld\"),\n+\t\t\t\t    graph->num_commits, expected_size,\n+\t\t\t\t    graph->chunk_mpbf_index_size);\n+\t\t\tgraph->num_modified_path_bloom_hashes = *graph->chunk_mpbf_index;\n+\t\t\tif (!graph->num_modified_path_bloom_hashes)\n+\t\t\t\tBUG(_(\"Modified Path Bloom Filter Index chunk using 0 hashes\"));\n+\t\t\tif (graph->chunk_mpbf_excludes)\n+\t\t\t\t/*\n+\t\t\t\t * Modified Path Bloom Filter Excludes are\n+\t\t\t\t * not supported yet.\n+\t\t\t\t */\n+\t\t\t\tgraph->chunk_mpbf_index = NULL;\n+\t\t} else\n+\t\t\tgraph->chunk_mpbf_index = NULL;\n+\t}\n+\n \treturn graph;\n }\n \n@@ -777,6 +818,68 @@ struct tree *get_commit_tree_in_graph(struct repository *r, const struct commit\n \treturn get_commit_tree_in_graph_one(r, r->objects->commit_graph, c);\n }\n \n+static int load_modified_path_bloom_filter_from_graph(\n+\t\tstruct commit_graph *graph, struct commit *commit,\n+\t\tstruct commit *parent, struct bloom_filter *bf)\n+{\n+\tconst uint8_t *bloom_index;\n+\tint first_parent = 0;\n+\n+\tif (commit->graph_pos == COMMIT_NOT_FROM_GRAPH)\n+\t\treturn 0;\n+\tif (!graph)\n+\t\treturn 0;\n+\tif (!graph->chunk_mpbf_index)\n+\t\treturn 0;\n+\n+\tif (!commit->parents || commit->parents->item == parent)\n+\t\tfirst_parent = 1;\n+\n+\tbloom_index = graph->chunk_mpbf_index + sizeof(uint8_t) +\n+\t\t      sizeof(uint64_t) * commit->graph_pos;\n+\n+\tif (bloom_index[0] & (1 << 7)) {\n+\t\tuint64_t v;\n+\t\tif (!first_parent)\n+\t\t\treturn 0;\n+\t\tmemcpy(&v, bloom_index, sizeof(v));\n+\t\tif (v == GRAPH_MODIFIED_PATH_BLOOM_FILTER_NONE)\n+\t\t\treturn 0;\n+\n+\t\t/* embedded modified path Bloom filter */\n+\t\tbf->nr_bits = GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS;\n+\t\tbf->bits = (uint8_t*) bloom_index;\n+\t\treturn 1;\n+\t}\n+\t/* support for non-embedded Bloom filters is not implemented yet. */\n+\treturn 0;\n+}\n+\n+enum bloom_result check_modified_path_bloom_filter(struct repository *r,\n+\t\tstruct pathspec *pathspec, struct commit *parent,\n+\t\tstruct commit *commit)\n+{\n+\tstruct bloom_filter bf;\n+\tint i;\n+\n+\tif (!pathspec->can_use_modified_path_bloom_filters)\n+\t\treturn BLOOM_CANT_TELL;\n+\tif (!load_modified_path_bloom_filter_from_graph(r->objects->commit_graph,\n+\t\t\t\t\t\t\tcommit, parent, &bf))\n+\t\treturn BLOOM_CANT_TELL;\n+\n+\tfor (i = 0; i < pathspec->nr; i++) {\n+\t\tstruct pathspec_item *pi = &pathspec->items[i];\n+\n+\t\tif (bloom_filter_check_bits(&bf,\n+\t\t\t\t\tpi->modified_path_bloom_hashes,\n+\t\t\t\t\tpi->modified_path_bloom_hashes_nr))\n+\t\t\treturn BLOOM_POSSIBLY_YES;\n+\t}\n+\n+\treturn BLOOM_DEFINITELY_NOT;\n+}\n+\n static uint32_t modified_path_bloom_seeds[] = {\n \t0xe83c5163U,\n \t0x3b376b0cU,\n@@ -807,6 +910,49 @@ static void compute_modified_path_bloom_hashes_for_path(const char *path,\n \t}\n }\n \n+void init_pathspec_bloom_fields(struct repository *r,\n+\t\t\t\tstruct pathspec *pathspec)\n+{\n+\tconst unsigned bloom_compatible_magic = PATHSPEC_LITERAL;\n+\tstruct commit_graph *graph = r->objects->commit_graph;\n+\tint i;\n+\n+\tif (!graph)\n+\t\treturn;\n+\tif (!graph->chunk_mpbf_index)\n+\t\treturn;\n+\tif (!pathspec->nr)\n+\t\treturn;\n+\tif (pathspec->has_wildcard)\n+\t\treturn;\n+\tif (pathspec->magic & ~bloom_compatible_magic)\n+\t\treturn;\n+\n+\tfor (i = 0; i < pathspec->nr; i++) {\n+\t\tstruct pathspec_item *pi = &pathspec->items[i];\n+\t\tconst char *path = pi->match;\n+\t\tsize_t len = pi->len;\n+\n+\t\t/*\n+\t\t * Pathspec parsing has normalized away any consecutive\n+\t\t * slashes, but a trailing slash might still be present,\n+\t\t * \"remove\" it.\n+\t\t */\n+\t\tif (path[len - 1] == '/')\n+\t\t\tlen--;\n+\n+\t\tpi->modified_path_bloom_hashes_nr = graph->num_modified_path_bloom_hashes;\n+\t\tALLOC_ARRAY(pi->modified_path_bloom_hashes,\n+\t\t\t    pi->modified_path_bloom_hashes_nr);\n+\n+\t\tcompute_modified_path_bloom_hashes_for_path(path, len,\n+\t\t\t\tgraph->num_modified_path_bloom_hashes,\n+\t\t\t\tpi->modified_path_bloom_hashes);\n+\t}\n+\n+\tpathspec->can_use_modified_path_bloom_filters = 1;\n+}\n+\n struct packed_commit_list {\n \tstruct commit **list;\n \tint nr;\ndiff --git a/commit-graph.h b/commit-graph.h\nindex 847cd25cfc..09dfc16932 100644\n--- a/commit-graph.h\n+++ b/commit-graph.h\n@@ -11,6 +11,8 @@ struct commit;\n struct repository;\n struct raw_object_store;\n struct string_list;\n+struct bloom_filter;\n+struct pathspec;\n \n char *get_commit_graph_filename(const char *obj_dir);\n int open_commit_graph(const char *graph_file, int *fd, struct stat *st);\n@@ -38,6 +40,12 @@ void load_commit_graph_info(struct repository *r, struct commit *item);\n struct tree *get_commit_tree_in_graph(struct repository *r,\n \t\t\t\t      const struct commit *c);\n \n+enum bloom_result check_modified_path_bloom_filter(struct repository *r,\n+\t\tstruct pathspec *pathspec, struct commit *parent,\n+\t\tstruct commit *commit);\n+void init_pathspec_bloom_fields(struct repository *r,\n+\t\tstruct pathspec *pathspec);\n+\n struct commit_graph {\n \tint graph_fd;\n \n@@ -59,6 +67,11 @@ struct commit_graph {\n \tconst unsigned char *chunk_commit_data;\n \tconst unsigned char *chunk_extra_edges;\n \tconst unsigned char *chunk_base_graphs;\n+\tconst unsigned char *chunk_mpbf_index;\n+\tuint64_t chunk_mpbf_index_size;\n+\tconst unsigned char *chunk_mpbf_excludes;\n+\n+\tuint8_t num_modified_path_bloom_hashes;\n };\n \n struct commit_graph *load_commit_graph_one_fd_st(int fd, struct stat *st);\ndiff --git a/pathspec.c b/pathspec.c\nindex 128f27fcb7..01e7ae6944 100644\n--- a/pathspec.c\n+++ b/pathspec.c\n@@ -406,6 +406,8 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags,\n \titem->attr_check = NULL;\n \titem->attr_match = NULL;\n \titem->attr_match_nr = 0;\n+\titem->modified_path_bloom_hashes_nr = 0;\n+\titem->modified_path_bloom_hashes = NULL;\n \n \t/* PATHSPEC_LITERAL_PATH ignores magic */\n \tif (flags & PATHSPEC_LITERAL_PATH) {\n@@ -674,6 +676,12 @@ void copy_pathspec(struct pathspec *dst, const struct pathspec *src)\n \t\t}\n \n \t\td->attr_check = attr_check_dup(s->attr_check);\n+\n+\t\tALLOC_ARRAY(d->modified_path_bloom_hashes,\n+\t\t\t    d->modified_path_bloom_hashes_nr);\n+\t\tCOPY_ARRAY(d->modified_path_bloom_hashes,\n+\t\t\t   s->modified_path_bloom_hashes,\n+\t\t\t   d->modified_path_bloom_hashes_nr);\n \t}\n }\n \n@@ -684,6 +692,7 @@ void clear_pathspec(struct pathspec *pathspec)\n \tfor (i = 0; i < pathspec->nr; i++) {\n \t\tfree(pathspec->items[i].match);\n \t\tfree(pathspec->items[i].original);\n+\t\tfree(pathspec->items[i].modified_path_bloom_hashes);\n \n \t\tfor (j = 0; j < pathspec->items[i].attr_match_nr; j++)\n \t\t\tfree(pathspec->items[i].attr_match[j].value);\ndiff --git a/pathspec.h b/pathspec.h\nindex 454ce364fa..0c661e1b03 100644\n--- a/pathspec.h\n+++ b/pathspec.h\n@@ -32,6 +32,7 @@ struct pathspec {\n \tunsigned int has_wildcard:1;\n \tunsigned int recursive:1;\n \tunsigned int recurse_submodules:1;\n+\tunsigned int can_use_modified_path_bloom_filters:1;\n \tunsigned magic;\n \tint max_depth;\n \tstruct pathspec_item {\n@@ -52,6 +53,17 @@ struct pathspec {\n \t\t\t} match_mode;\n \t\t} *attr_match;\n \t\tstruct attr_check *attr_check;\n+\n+\t\t/*\n+\t\t * These fields are only relevant during pathspec-limited\n+\t\t * revision walks, init_pathspec_item() leaves them\n+\t\t * zero-initialized, but copy_pathspec() copies them,\n+\t\t * and clear_pathspec() releases the allocated memory.\n+\t\t * IOW: they are only valid if 'struct pathspec's\n+\t\t * 'can_use_modified_path_bloom_filters' bit is set.\n+\t\t */\n+\t\tuint32_t modified_path_bloom_hashes_nr;\n+\t\tuint32_t *modified_path_bloom_hashes;\n \t} *items;\n };\n \ndiff --git a/revision.c b/revision.c\nindex 9dac1262ef..6ec4bc0cb1 100644\n--- a/revision.c\n+++ b/revision.c\n@@ -29,6 +29,7 @@\n #include \"prio-queue.h\"\n #include \"hashmap.h\"\n #include \"utf8.h\"\n+#include \"bloom-filter.h\"\n \n volatile show_early_output_fn_t show_early_output;\n \n@@ -629,6 +630,7 @@ static int rev_compare_tree(struct rev_info *revs,\n {\n \tstruct tree *t1 = get_commit_tree(parent);\n \tstruct tree *t2 = get_commit_tree(commit);\n+\tenum bloom_result bloom_ret;\n \n \tif (!t1)\n \t\treturn REV_TREE_NEW;\n@@ -653,6 +655,12 @@ static int rev_compare_tree(struct rev_info *revs,\n \t\t\treturn REV_TREE_SAME;\n \t}\n \n+\tbloom_ret = check_modified_path_bloom_filter(revs->repo,\n+\t\t\t\t\t\t     &revs->pruning.pathspec,\n+\t\t\t\t\t\t     parent, commit);\n+\tif (bloom_ret == BLOOM_DEFINITELY_NOT)\n+\t\treturn REV_TREE_SAME;\n+\n \ttree_difference = REV_TREE_SAME;\n \trevs->pruning.flags.has_changes = 0;\n \tdiff_tree_oid(&t1->object.oid, &t2->object.oid, \"\", &revs->pruning);\n@@ -662,10 +670,17 @@ static int rev_compare_tree(struct rev_info *revs,\n static int rev_same_tree_as_empty(struct rev_info *revs, struct commit *commit)\n {\n \tstruct tree *t1 = get_commit_tree(commit);\n+\tenum bloom_result bloom_ret;\n \n \tif (!t1)\n \t\treturn 0;\n \n+\tbloom_ret = check_modified_path_bloom_filter(revs->repo,\n+\t\t\t\t\t\t     &revs->pruning.pathspec,\n+\t\t\t\t\t\t     NULL, commit);\n+\tif (bloom_ret == BLOOM_DEFINITELY_NOT)\n+\t\treturn 1;\n+\n \ttree_difference = REV_TREE_SAME;\n \trevs->pruning.flags.has_changes = 0;\n \tdiff_tree_oid(NULL, &t1->object.oid, \"\", &revs->pruning);\n@@ -3376,6 +3391,9 @@ int prepare_revision_walk(struct rev_info *revs)\n \t\tsimplify_merges(revs);\n \tif (revs->children.name)\n \t\tset_children(revs);\n+\n+\tinit_pathspec_bloom_fields(revs->repo, &revs->pruning.pathspec);\n+\n \treturn 0;\n }\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398863","messageId":"20200529085038.26008-28-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 27/34] commit-graph: load modified path Bloom filters for merge commits","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:31Z","receivedAt":"2020-05-29T08:51:46Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"---\n commit-graph.c | 53 +++++++++++++++++++++++++++++++++++++++++++++-----\n commit-graph.h |  2 ++\n 2 files changed, 50 insertions(+), 5 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 178bbf0113..32738ba4b8 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -26,6 +26,7 @@\n #define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* \"EDGE\" */\n #define GRAPH_CHUNKID_BASE 0x42415345 /* \"BASE\" */\n #define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX 0x4d504249 /* \"MPBI\" */\n+#define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_MERGE_INDEX 0x4d50424d /* \"MPBM\" */\n #define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTERS 0x4d504246 /* \"MPBF\" */\n #define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_EXCLUDES 0x4d504258 /* \"MPBX\" */\n \n@@ -328,6 +329,15 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\t\t}\n \t\t\tbreak;\n \n+\t\tcase GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_MERGE_INDEX:\n+\t\t\tif (graph->chunk_mpbf_merge_index)\n+\t\t\t\tchunk_repeated = 1;\n+\t\t\telse {\n+\t\t\t\tgraph->chunk_mpbf_merge_index = data + chunk_offset;\n+\t\t\t\tgraph->chunk_mpbf_merge_index_size = next_chunk_offset - chunk_offset;\n+\t\t\t}\n+\t\t\tbreak;\n+\n \t\tcase GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTERS:\n \t\t\tif (graph->chunk_mpbf_filters)\n \t\t\t\tchunk_repeated = 1;\n@@ -869,11 +879,44 @@ static int load_modified_path_bloom_filter_from_graph(\n \t\tbf->bits = (uint8_t*) bloom_index;\n \t\treturn 1;\n \t} else if (bloom_index[0] & (1 << 6)) {\n-\t\t/*\n-\t\t * Modified path Bloom filters for second..nth parents of\n-\t\t * merge commits are not implemented yet.\n-\t\t */\n-\t\treturn 0;\n+\t\tstruct commit_list *p;\n+\t\tuint32_t pos;\n+\t\tint found = 0;\n+\n+\t\tpos = get_be32(bloom_index + sizeof(uint32_t));\n+\n+\t\tif (!graph->chunk_mpbf_merge_index)\n+\t\t\tBUG(\"commit %s refers to position %u in the Modified Path Bloom Filter Merge Index chunk, but that chunk is missing\",\n+\t\t\t    oid_to_hex(&commit->object.oid), pos);\n+\n+\t\tfor (p = commit->parents; p; p = p->next, pos++)\n+\t\t\tif (p->item == parent) {\n+\t\t\t\tfound = 1;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\tif (!found)\n+\t\t\tBUG(\"commit %s has no parent %s\\n\",\n+\t\t\t    oid_to_hex(&commit->object.oid),\n+\t\t\t    oid_to_hex(&parent->object.oid));\n+\n+\t\tif (pos * sizeof(uint64_t) > graph->chunk_mpbf_merge_index_size)\n+\t\t\tBUG(\"commit %s and parent %s refer to position %u in the Modified Path Bloom Filter Merge Index chunk, but that's too large for a chunk of size %lu bytes\",\n+\t\t\t    oid_to_hex(&commit->object.oid),\n+\t\t\t    oid_to_hex(&parent->object.oid), pos,\n+\t\t\t    graph->chunk_mpbf_merge_index_size);\n+\t\tbloom_index = graph->chunk_mpbf_merge_index + sizeof(uint64_t) * pos;\n+\t\tif (bloom_index[0] & (1 << 7)) {\n+\t\t\tuint64_t v;\n+\t\t\tmemcpy(&v, bloom_index, sizeof(v));\n+\t\t\tif (v == GRAPH_MODIFIED_PATH_BLOOM_FILTER_NONE)\n+\t\t\t\treturn 0;\n+\n+\t\t\t/* embedded modified path Bloom filter */\n+\t\t\tbf->nr_bits = GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS;\n+\t\t\tbf->bits = (uint8_t*) bloom_index;\n+\t\t\treturn 1;\n+\t\t} else\n+\t\t\toffset = get_be64(bloom_index);\n \t} else {\n \t\tif (!first_parent)\n \t\t\treturn 0;\ndiff --git a/commit-graph.h b/commit-graph.h\nindex cde0d7fa30..b2605a0e3b 100644\n--- a/commit-graph.h\n+++ b/commit-graph.h\n@@ -69,6 +69,8 @@ struct commit_graph {\n \tconst unsigned char *chunk_base_graphs;\n \tconst unsigned char *chunk_mpbf_index;\n \tuint64_t chunk_mpbf_index_size;\n+\tconst unsigned char *chunk_mpbf_merge_index;\n+\tuint64_t chunk_mpbf_merge_index_size;\n \tconst unsigned char *chunk_mpbf_filters;\n \tuint64_t chunk_mpbf_filters_size;\n \tconst unsigned char *chunk_mpbf_excludes;\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398864","messageId":"20200529085038.26008-29-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 28/34] commit-graph: write Modified Path Bloom Filter Merge Index chunk","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:32Z","receivedAt":"2020-05-29T08:51:47Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"[Explain!]\n\nThe table below compares the runtime of\n\n  git rev-list --full-history HEAD -- \"$path\"\n\nwith modified path Bloom filters for only first parents and with all\nparents of merge commits for 5000+ randomly selected paths from each\nrepository.  It also shows the size increase of the commit-graph file:\n\n                          |    Average runtime            |   commit-graph file size\n                          |                               |\n               Percentage |    first      all             |   first     all\n                of merge  |   parent     merge    Average |  parent    merge     Size\n                 commits  |    only     parents   speedup |   only    parents  increase\n  ------------------------+-------------------------------+----------------------------\n  android-base    73.6%   |  7.0890s    0.9681s    7.32x  |  29.8MB   218.0MB   7.32x\n  cmssw           11.0%   |  0.7164s    0.2577s    2.78x  |  22.9MB    51.8MB   2.26x\n  cpython         11.7%   |  0.3815s    0.1460s    2.61x  |   7.8MB    10.6MB   1.36x\n  elasticsearch    8.4%   |  0.1217s    0.0582s    2.09x  |   9.6MB    10.9MB   1.14x\n  gecko-dev        3.5%   |  1.6432s    0.9689s    1.70x  |  63.5MB    90.4MB   1.42x\n  git             25.3%   |  0.8743s    0.1729s    5.06x  |   3.8MB    10.4MB   2.74x\n  homebrew-cask    9.6%   |  1.1454s    0.1846s    6.20x  |   6.5MB     7.2MB   1.10x\n  jdk             25.0%   |  0.2598s    0.1242s    2.09x  |  15.1MB    20.3MB   1.34x\n  linux            7.4%   |  2.8037s    1.3339s    2.10x  |  78.1MB   269.8MB   3.45x\n  rails           22.2%   |  0.2854s    0.0926s    3.08x  |   6.0MB     8.4MB   1.40x\n  rust            27.0%   |  1.9803s    0.1794s   11.04x  |   8.2MB    22.0MB   2.68x\n  tensorflow       9.1%   |  0.3886s    0.1237s    3.14x  |   9.0MB    19.0MB   2.11x\n\nStoring modified path Bloom filters for all parents of merge commits\nsignificantly increases the size of the Modified Path Bloom Filters\nchunk, though when looking at the size of the whole commit-graph file\nthe relative increase is sometimes not that much.  FWIW, the average\nspeedup in most cases (except android-base and linux) is higher than\nthe size increase of the commit-graph file.\n\nBeware!  To create a Bloom filter from the paths modified between a\ncommit and one of its parents, it seems natural to extend\ncreate_modified_path_bloom_filter() with a 'struct commit *parent'\nparameter...  and then things would blow up when encountering commit\na733a5da97b2 (Merge branches 'release' and 'fluff' into release,\n2008-02-07) in linux.git:\n\n  $ git -C linux.git log -1 a733a5da97b2 |head -n2\n  commit a733a5da97b238e3e3167d3d0aee8fe1e8d04e97\n  Merge: 299cfe38081b 299cfe38081b 9e52797131e8\n\nNotice how the same commit is both the first _and_ second parent!  So\nextend create_modified_path_bloom_filter()'s signature with the int\nindex of the particular parent.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n Documentation/config/core.txt |  13 +++\n commit-graph.c                | 169 +++++++++++++++++++++++++++++-----\n 2 files changed, 158 insertions(+), 24 deletions(-)\n\ndiff --git a/Documentation/config/core.txt b/Documentation/config/core.txt\nindex 820f7d3bcf..b4311c5f31 100644\n--- a/Documentation/config/core.txt\n+++ b/Documentation/config/core.txt\n@@ -594,6 +594,19 @@ core.modifiedPathBloomFilters::\n \tcommit-graph file to speed up pathspec-limited revision walks.\n \tDefaults to false.\n \n+core.modifiedPathBloomFiltersForAllMergeParents::\n+\tEXPERIMENTAL!!!\n+\tIf true, then Git will store Bloom filters for all parents of\n+\tmerge commits in the commit-graph file.\n+\tThis has no impact on repositories with linear history.  In\n+\trepositories with lots of merge commits this considerably\n+\tincreases the runtime of writing the commit-graph file and its\n+\tsize.  It has little benefit in pathspec-limited revision walks\n+\twith the default history simplification, but can speed up\n+\t`--full-history` considerably.\n+\tDefaults to false, ignored if `core.modifiedPathBloomFilters` is\n+\tfalse.\n+\n core.useReplaceRefs::\n \tIf set to `false`, behave as if the `--no-replace-objects`\n \toption was given on the command line. See linkgit:git[1] and\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 32738ba4b8..9cce79d452 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -65,12 +65,23 @@ struct modified_path_bloom_filter_info {\n \t * -1 before that.\n \t */\n \tuint64_t offset;\n+\tuint32_t merge_index_pos;\n+\tstruct modified_path_bloom_filter_info *next;\n };\n \n static void free_modified_path_bloom_filter_info_in_slab(\n \t\tstruct modified_path_bloom_filter_info *bfi)\n {\n+\t/* The instance got as parameter is on the slab, don't free() it. */\n \tbloom_filter_free(&bfi->filter);\n+\tbfi = bfi->next;\n+\t/* The rest is in a linked list, and must be free()d. */\n+\twhile (bfi) {\n+\t\tstruct modified_path_bloom_filter_info *prev = bfi;\n+\t\tbloom_filter_free(&bfi->filter);\n+\t\tbfi = bfi->next;\n+\t\tfree(prev);\n+\t}\n }\n \n define_commit_slab(modified_path_bloom_filters,\n@@ -1115,7 +1126,8 @@ struct write_commit_graph_context {\n \tconst struct split_commit_graph_opts *split_opts;\n \n \tstruct modified_path_bloom_filter_context {\n-\t\tunsigned use_modified_path_bloom_filters:1;\n+\t\tunsigned use_modified_path_bloom_filters:1,\n+\t\t\t all_merge_parents:1;\n \t\tunsigned int num_hashes;\n \t\t/*\n \t\t * Number of paths to be added to \"embedded\" modified path\n@@ -1143,6 +1155,7 @@ struct write_commit_graph_context {\n \n \t\t/* Excluding embedded modified path Bloom filters */\n \t\tuint64_t total_filter_size;\n+\t\tuint32_t num_merge_index_entries;\n \n \t\t/* Used to find identical modified path Bloom filters */\n \t\tstruct hashmap dedup_hashmap;\n@@ -1361,28 +1374,70 @@ static int write_graph_chunk_modified_path_bloom_filters(struct hashfile *f,\n \n \t\tbfi = modified_path_bloom_filters_peek(\n \t\t\t\t&modified_path_bloom_filters, commit);\n+\t\tfor (; bfi; bfi = bfi->next) {\n+\t\t\tif (bfi->duplicate_of)\n+\t\t\t\tcontinue;\n+\t\t\tif (!bfi->filter.nr_bits)\n+\t\t\t\tcontinue;\n+\t\t\tif (bfi->filter.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS)\n+\t\t\t\tcontinue;\n \n-\t\tif (!bfi)\n-\t\t\tcontinue;\n-\t\tif (bfi->duplicate_of)\n-\t\t\tcontinue;\n-\t\tif (!bfi->filter.nr_bits)\n-\t\t\tcontinue;\n-\t\tif (bfi->filter.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS)\n-\t\t\tcontinue;\n+\t\t\tif (offset >> 62)\n+\t\t\t\tBUG(\"offset %lu is too large for the Modified Path Bloom Filter Index chunk\",\n+\t\t\t\t    offset);\n+\n+\t\t\tbfi->offset = offset;\n+\n+\t\t\tfilter_size = bloom_filter_bytes(&bfi->filter);\n+\n+\t\t\thashwrite_be32(f, bfi->filter.nr_bits);\n+\t\t\thashwrite(f, bfi->filter.bits, filter_size);\n+\n+\t\t\toffset += sizeof(uint32_t) + filter_size;\n+\t\t}\n+\t}\n+\treturn 0;\n+}\n \n-\t\tif (offset >> 62)\n-\t\t\tBUG(\"offset %lu is too large for the Modified Path Bloom Filter Index chunk\",\n-\t\t\t    offset);\n+static int write_graph_chunk_modified_path_bloom_merge_index(\n+\t\tstruct hashfile *f, struct write_commit_graph_context *ctx)\n+{\n+\tconst uint64_t no_bloom_filter = GRAPH_MODIFIED_PATH_BLOOM_FILTER_NONE;\n+\tuint32_t pos = 0;\n+\tint i;\n \n-\t\tbfi->offset = offset;\n+\tfor (i = 0; i < ctx->commits.nr; i++) {\n+\t\tstruct commit *commit = ctx->commits.list[i];\n+\t\tstruct modified_path_bloom_filter_info *bfi;\n \n-\t\tfilter_size = bloom_filter_bytes(&bfi->filter);\n+\t\tdisplay_progress(ctx->progress, ++ctx->progress_cnt);\n \n-\t\thashwrite_be32(f, bfi->filter.nr_bits);\n-\t\thashwrite(f, bfi->filter.bits, filter_size);\n+\t\tbfi = modified_path_bloom_filters_peek(\n+\t\t\t\t&modified_path_bloom_filters, commit);\n \n-\t\toffset += sizeof(uint32_t) + filter_size;\n+\t\tif (!bfi || !bfi->next)\n+\t\t\t/* No merge index entries for this commit. */\n+\t\t\tcontinue;\n+\n+\t\tdo {\n+\t\t\tif (bfi->duplicate_of) {\n+\t\t\t\tuint64_t offset = htonll(bfi->duplicate_of->offset);\n+\t\t\t\thashwrite(f, &offset, sizeof(offset));\n+\t\t\t} else if (!bfi->filter.nr_bits) {\n+\t\t\t\thashwrite(f, &no_bloom_filter,\n+\t\t\t\t\t  sizeof(no_bloom_filter));\n+\t\t\t} else if (bfi->filter.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS) {\n+\t\t\t\tuint8_t filterdata[sizeof(uint64_t)];\n+\t\t\t\tmemcpy(filterdata, bfi->filter.bits, sizeof(filterdata));\n+\t\t\t\tfilterdata[0] |= 1 << 7;\n+\t\t\t\thashwrite(f, filterdata, sizeof(filterdata));\n+\t\t\t} else if (bfi->offset != -1) {\n+\t\t\t\tuint64_t offset = htonll(bfi->offset);\n+\t\t\t\thashwrite(f, &offset, sizeof(offset));\n+\t\t\t} else\n+\t\t\t\tBUG(\"modified path Bloom filter offset is still -1?!\");\n+\t\t\tbfi->merge_index_pos = pos++;\n+\t\t} while ((bfi = bfi->next));\n \t}\n \treturn 0;\n }\n@@ -1403,7 +1458,19 @@ static int write_graph_chunk_modified_path_bloom_index(struct hashfile *f,\n \t\tbfi = modified_path_bloom_filters_peek(\n \t\t\t\t&modified_path_bloom_filters, commit);\n \n-\t\tif (!bfi || !bfi->filter.nr_bits) {\n+\t\tif (!bfi) {\n+\t\t\thashwrite(f, &no_bloom_filter, sizeof(no_bloom_filter));\n+\t\t} else if (bfi->next) {\n+\t\t\tuint8_t filterdata[sizeof(uint64_t)];\n+\t\t\tif (!commit->parents->next)\n+\t\t\t\tBUG(\"uh-oh #1\");\n+\t\t\tif (bfi->merge_index_pos == -1)\n+\t\t\t\tBUG(\"uh-oh #2\");\n+\t\t\tmemset(filterdata, 0, sizeof(filterdata));\n+\t\t\tfilterdata[0] |= 1 << 6;\n+\t\t\t((uint32_t*)filterdata)[1] = htonl(bfi->merge_index_pos);\n+\t\t\thashwrite(f, filterdata, sizeof(filterdata));\n+\t\t} else if (!bfi->filter.nr_bits) {\n \t\t\thashwrite(f, &no_bloom_filter, sizeof(no_bloom_filter));\n \t\t} else if (bfi->filter.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS) {\n \t\t\tuint8_t filterdata[sizeof(uint64_t)];\n@@ -1618,14 +1685,18 @@ static int handle_duplicate_modified_path_bloom_filter(\n \n static void create_modified_path_bloom_filter(\n \t\tstruct modified_path_bloom_filter_context *mpbfctx,\n-\t\tstruct commit *commit)\n+\t\tstruct commit *commit, int nth_parent)\n {\n \tstruct modified_path_bloom_filter_info *bfi;\n \tstruct object_id *parent_oid;\n \tuintmax_t path_component_count;\n+\tint i;\n \n \tif (!mpbfctx->use_modified_path_bloom_filters)\n \t\treturn;\n+\tif (nth_parent &&\n+\t    !mpbfctx->all_merge_parents)\n+\t\treturn;\n \n \t/*\n \t * This function can be called multiple times for the same commit\n@@ -1635,11 +1706,25 @@ static void create_modified_path_bloom_filter(\n \t */\n \tbfi = modified_path_bloom_filters_peek(&modified_path_bloom_filters,\n \t\t\t\t\t       commit);\n-\tif (bfi && bfi->filter.nr_bits)\n+\tfor (i = 0; i < nth_parent && bfi; i++, bfi = bfi->next);\n+\tif (i == nth_parent && bfi && bfi->filter.nr_bits)\n \t\treturn;\n \n-\tparent_oid = commit->parents ? &commit->parents->item->object.oid :\n-\t\t\t\t       NULL;\n+\tif (commit->parents) {\n+\t\tstruct commit_list *p;\n+\t\tfor (i = 0, p = commit->parents;\n+\t\t     i < nth_parent && p;\n+\t\t     i++, p = p->next);\n+\t\tif (!p)\n+\t\t\tBUG(\"couldn't find %dth parent of commit %s\\n\",\n+\t\t\t    nth_parent + 1, oid_to_hex(&commit->object.oid));\n+\t\tparent_oid = &p->item->object.oid;\n+\t} else {\n+\t\tif (nth_parent)\n+\t\t\tBUG(\"looking for the %dth parent of commit %s with no parents\",\n+\t\t\t    nth_parent + 1, oid_to_hex(&commit->object.oid));\n+\t\tparent_oid = NULL;\n+\t}\n \tmpbfctx->hashes_nr = 0;\n \tstrbuf_reset(&mpbfctx->prev_path);\n \tdiff_tree_oid(parent_oid, &commit->object.oid, \"\", &mpbfctx->diffopt);\n@@ -1647,7 +1732,23 @@ static void create_modified_path_bloom_filter(\n \n \tbfi = modified_path_bloom_filters_at(&modified_path_bloom_filters,\n \t\t\t\t\t     commit);\n+\tif (!nth_parent) {\n+\t\t/* This is the right bloom_filter_info instance. */\n+\t\tif (mpbfctx->all_merge_parents &&\n+\t\t    commit->parents && commit->parents->next)\n+\t\t\tmpbfctx->num_merge_index_entries++;\n+\t} else {\n+\t\t/* i is one step ahead of bfi */\n+\t\tfor (i = 1; i < nth_parent && bfi; i++, bfi = bfi->next);\n+\t\tif (bfi->next)\n+\t\t\tBUG(\"Huh?!?\");\n+\t\tbfi->next = xcalloc(1, sizeof(*bfi));\n+\t\tbfi = bfi->next;\n+\t\tmpbfctx->num_merge_index_entries++;\n+\t}\n+\n \tbfi->offset = -1;\n+\tbfi->merge_index_pos = -1;\n \tif (path_component_count > mpbfctx->embedded_limit)\n \t\tbloom_filter_init(&bfi->filter, mpbfctx->num_hashes,\n \t\t\t\t  path_component_count);\n@@ -1667,10 +1768,20 @@ static void create_modified_path_bloom_filter(\n static void add_missing_parents(struct write_commit_graph_context *ctx, struct commit *commit)\n {\n \tstruct commit_list *parent;\n+\tint nth_parent;\n+\n+\tif (!commit->parents) {\n+\t\t/* root commit */\n+\t\tcreate_modified_path_bloom_filter(&ctx->mpbfctx, commit, 0);\n+\t\treturn;\n+\t}\n \n-\tcreate_modified_path_bloom_filter(&ctx->mpbfctx, commit);\n+\tfor (parent = commit->parents, nth_parent = 0;\n+\t     parent;\n+\t     parent = parent->next, nth_parent++) {\n+\t\tcreate_modified_path_bloom_filter(&ctx->mpbfctx, commit,\n+\t\t\t\t\t\t  nth_parent);\n \n-\tfor (parent = commit->parents; parent; parent = parent->next) {\n \t\tif (!(parent->item->object.flags & REACHABLE)) {\n \t\t\tALLOC_GROW(ctx->oids.list, ctx->oids.nr + 1, ctx->oids.alloc);\n \t\t\toidcpy(&ctx->oids.list[ctx->oids.nr], &(parent->item->object.oid));\n@@ -2079,6 +2190,13 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\t\tchunks[chunks_nr].write_fn = write_graph_chunk_modified_path_bloom_filters;\n \t\t\tchunks_nr++;\n \t\t}\n+\t\tif (ctx->mpbfctx.num_merge_index_entries) {\n+\t\t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\t\t\tchunks[chunks_nr].id = GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_MERGE_INDEX;\n+\t\t\tchunks[chunks_nr].size = ctx->mpbfctx.num_merge_index_entries * sizeof(uint64_t);\n+\t\t\tchunks[chunks_nr].write_fn = write_graph_chunk_modified_path_bloom_merge_index;\n+\t\t\tchunks_nr++;\n+\t\t}\n \t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \t\tchunks[chunks_nr].id = GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX;\n \t\tchunks[chunks_nr].size = sizeof(uint8_t) +\n@@ -2487,6 +2605,9 @@ int write_commit_graph(const char *obj_dir,\n \t\twarning(\"not writing modified path Bloom filters with --split\");\n \t} else if (res) {\n \t\tctx->mpbfctx.use_modified_path_bloom_filters = 1;\n+\t\tif (!git_config_get_bool(\"core.modifiedPathBloomFiltersForAllMergeParents\", &res) &&\n+\t\t    res)\n+\t\t\tctx->mpbfctx.all_merge_parents = 1;\n \t\tctx->mpbfctx.num_hashes = GRAPH_MODIFIED_PATH_BLOOM_FILTER_DEFAULT_NR_HASHES;\n \t\tctx->mpbfctx.embedded_limit = GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS / (ctx->mpbfctx.num_hashes * 10 / 7);\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398865","messageId":"20200529085038.26008-31-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 30/34] commit-graph: move write_commit_graph_reachable below write_commit_graph","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:34Z","receivedAt":"2020-05-29T08:51:49Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"---\n commit-graph.c | 30 +++++++++++++++---------------\n 1 file changed, 15 insertions(+), 15 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex b5cd4c863a..4cbfce61bf 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1901,21 +1901,6 @@ static int add_ref_to_list(const char *refname,\n \treturn 0;\n }\n \n-int write_commit_graph_reachable(const char *obj_dir,\n-\t\t\t\t enum commit_graph_write_flags flags,\n-\t\t\t\t const struct split_commit_graph_opts *split_opts)\n-{\n-\tstruct string_list list = STRING_LIST_INIT_DUP;\n-\tint result;\n-\n-\tfor_each_ref(add_ref_to_list, &list);\n-\tresult = write_commit_graph(obj_dir, NULL, &list,\n-\t\t\t\t    flags, split_opts);\n-\n-\tstring_list_clear(&list, 0);\n-\treturn result;\n-}\n-\n static int fill_oids_from_packs(struct write_commit_graph_context *ctx,\n \t\t\t\tstruct string_list *pack_indexes)\n {\n@@ -2784,6 +2769,21 @@ int write_commit_graph(const char *obj_dir,\n \treturn res;\n }\n \n+int write_commit_graph_reachable(const char *obj_dir,\n+\t\t\t\t enum commit_graph_write_flags flags,\n+\t\t\t\t const struct split_commit_graph_opts *split_opts)\n+{\n+\tstruct string_list list = STRING_LIST_INIT_DUP;\n+\tint result;\n+\n+\tfor_each_ref(add_ref_to_list, &list);\n+\tresult = write_commit_graph(obj_dir, NULL, &list,\n+\t\t\t\t    flags, split_opts);\n+\n+\tstring_list_clear(&list, 0);\n+\treturn result;\n+}\n+\n #define VERIFY_COMMIT_GRAPH_ERROR_HASH 2\n static int verify_commit_graph_error;\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398866","messageId":"20200529085038.26008-30-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 29/34] commit-graph: extract init and free write_commit_graph_context","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:33Z","receivedAt":"2020-05-29T08:51:51Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The write_commit_graph() function is just over 180 lines long, among\nwhich 86 lines initialize the 'struct write_commit_graph_context'\ninstance and 32 lines free its resources.\n\nExtract these into init_write_commit_graph_context() and\nfree_write_commit_graph_context() functions, respectively, to make\nwrite_commit_graph() look much simpler, and, more importantly, to be\nable to reuse those init and free functions in the next patch.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 102 ++++++++++++++++++++++++++++++-------------------\n 1 file changed, 62 insertions(+), 40 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 9cce79d452..b5cd4c863a 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -2570,19 +2570,16 @@ static void expire_commit_graphs(struct write_commit_graph_context *ctx)\n \tstrbuf_release(&path);\n }\n \n-int write_commit_graph(const char *obj_dir,\n-\t\t       struct string_list *pack_indexes,\n-\t\t       struct string_list *commit_hex,\n-\t\t       enum commit_graph_write_flags flags,\n-\t\t       const struct split_commit_graph_opts *split_opts)\n+static struct write_commit_graph_context *init_write_commit_graph_context(\n+\t\tconst char *obj_dir, enum commit_graph_write_flags flags,\n+\t\tconst struct split_commit_graph_opts *split_opts)\n {\n \tstruct write_commit_graph_context *ctx;\n-\tuint32_t i, count_distinct = 0;\n \tsize_t len;\n \tint res = 0;\n \n \tif (!commit_graph_compatible(the_repository))\n-\t\treturn 0;\n+\t\treturn NULL;\n \n \tctx = xcalloc(1, sizeof(struct write_commit_graph_context));\n \tctx->r = the_repository;\n@@ -2637,6 +2634,7 @@ int write_commit_graph(const char *obj_dir,\n \t\t}\n \n \t\tif (ctx->num_commit_graphs_before) {\n+\t\t\tint i;\n \t\t\tALLOC_ARRAY(ctx->commit_graph_filenames_before, ctx->num_commit_graphs_before);\n \t\t\ti = ctx->num_commit_graphs_before;\n \t\t\tg = ctx->r->objects->commit_graph;\n@@ -2664,8 +2662,64 @@ int write_commit_graph(const char *obj_dir,\n \t\tctx->oids.alloc = 1024;\n \tALLOC_ARRAY(ctx->oids.list, ctx->oids.alloc);\n \n+\treturn ctx;\n+}\n+\n+static void free_write_commit_graph_context(\n+\t\tstruct write_commit_graph_context *ctx)\n+{\n+\tfree(ctx->graph_name);\n+\tfree(ctx->commits.list);\n+\tfree(ctx->oids.list);\n+\tfree(ctx->obj_dir);\n+\n+\tif (ctx->commit_graph_filenames_after) {\n+\t\tint i;\n+\t\tfor (i = 0; i < ctx->num_commit_graphs_after; i++) {\n+\t\t\tfree(ctx->commit_graph_filenames_after[i]);\n+\t\t\tfree(ctx->commit_graph_hash_after[i]);\n+\t\t}\n+\n+\t\tfor (i = 0; i < ctx->num_commit_graphs_before; i++)\n+\t\t\tfree(ctx->commit_graph_filenames_before[i]);\n+\n+\t\tfree(ctx->commit_graph_filenames_after);\n+\t\tfree(ctx->commit_graph_filenames_before);\n+\t\tfree(ctx->commit_graph_hash_after);\n+\t}\n+\n+\tif (ctx->mpbfctx.use_modified_path_bloom_filters) {\n+\t\tdeep_clear_modified_path_bloom_filters(\n+\t\t\t\t&modified_path_bloom_filters,\n+\t\t\t\tfree_modified_path_bloom_filter_info_in_slab);\n+\t\tstrbuf_release(&ctx->mpbfctx.prev_path);\n+\t\tfree(ctx->mpbfctx.hashed_prefix_lengths);\n+\t\tfree(ctx->mpbfctx.hashes);\n+\t\thashmap_free_entries(&ctx->mpbfctx.dedup_hashmap,\n+\t\t\t\tstruct modified_path_bloom_filter_dedup_entry,\n+\t\t\t\tentry);\n+\t}\n+\n+\tfree(ctx);\n+}\n+\n+int write_commit_graph(const char *obj_dir,\n+\t\t       struct string_list *pack_indexes,\n+\t\t       struct string_list *commit_hex,\n+\t\t       enum commit_graph_write_flags flags,\n+\t\t       const struct split_commit_graph_opts *split_opts)\n+{\n+\tstruct write_commit_graph_context *ctx;\n+\tuint32_t count_distinct = 0;\n+\tint res = 0;\n+\n+\tctx = init_write_commit_graph_context(obj_dir, flags, split_opts);\n+\tif (!ctx)\n+\t\treturn 0;\n+\n \tif (ctx->append && ctx->r->objects->commit_graph) {\n \t\tstruct commit_graph *g = ctx->r->objects->commit_graph;\n+\t\tint i;\n \t\tfor (i = 0; i < g->num_commits; i++) {\n \t\t\tconst unsigned char *hash = g->chunk_oid_lookup + g->hash_len * i;\n \t\t\thashcpy(ctx->oids.list[ctx->oids.nr++].hash, hash);\n@@ -2726,39 +2780,7 @@ int write_commit_graph(const char *obj_dir,\n \texpire_commit_graphs(ctx);\n \n cleanup:\n-\tfree(ctx->graph_name);\n-\tfree(ctx->commits.list);\n-\tfree(ctx->oids.list);\n-\tfree(ctx->obj_dir);\n-\n-\tif (ctx->commit_graph_filenames_after) {\n-\t\tfor (i = 0; i < ctx->num_commit_graphs_after; i++) {\n-\t\t\tfree(ctx->commit_graph_filenames_after[i]);\n-\t\t\tfree(ctx->commit_graph_hash_after[i]);\n-\t\t}\n-\n-\t\tfor (i = 0; i < ctx->num_commit_graphs_before; i++)\n-\t\t\tfree(ctx->commit_graph_filenames_before[i]);\n-\n-\t\tfree(ctx->commit_graph_filenames_after);\n-\t\tfree(ctx->commit_graph_filenames_before);\n-\t\tfree(ctx->commit_graph_hash_after);\n-\t}\n-\n-\tif (ctx->mpbfctx.use_modified_path_bloom_filters) {\n-\t\tdeep_clear_modified_path_bloom_filters(\n-\t\t\t\t&modified_path_bloom_filters,\n-\t\t\t\tfree_modified_path_bloom_filter_info_in_slab);\n-\t\tstrbuf_release(&ctx->mpbfctx.prev_path);\n-\t\tfree(ctx->mpbfctx.hashed_prefix_lengths);\n-\t\tfree(ctx->mpbfctx.hashes);\n-\t\thashmap_free_entries(&ctx->mpbfctx.dedup_hashmap,\n-\t\t\t\tstruct modified_path_bloom_filter_dedup_entry,\n-\t\t\t\tentry);\n-\t}\n-\n-\tfree(ctx);\n-\n+\tfree_write_commit_graph_context(ctx);\n \treturn res;\n }\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398867","messageId":"20200529085038.26008-32-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 31/34] t7007-show: make the first test compatible with the next patch","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:35Z","receivedAt":"2020-05-29T08:51:51Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Otherwise it would fail with GIT_TEST_COMMIT_GRAPH=1.\n---\n t/t7007-show.sh | 7 +++----\n 1 file changed, 3 insertions(+), 4 deletions(-)\n\ndiff --git a/t/t7007-show.sh b/t/t7007-show.sh\nindex 42d3db6246..93244cd718 100755\n--- a/t/t7007-show.sh\n+++ b/t/t7007-show.sh\n@@ -4,16 +4,15 @@ test_description='git show'\n \n . ./test-lib.sh\n \n-test_expect_success setup '\n+test_expect_success 'showing a tag that points at a missing object' '\n+\ttest_when_finished \"git tag -d foo-tag\" &&\n \techo hello world >foo &&\n \tH=$(git hash-object -w foo) &&\n \tgit tag -a foo-tag -m \"Tags $H\" $H &&\n \tHH=$(expr \"$H\" : \"\\(..\\)\") &&\n \tH38=$(expr \"$H\" : \"..\\(.*\\)\") &&\n-\trm -f .git/objects/$HH/$H38\n-'\n+\trm -f .git/objects/$HH/$H38 &&\n \n-test_expect_success 'showing a tag that point at a missing object' '\n \ttest_must_fail git --no-pager show foo-tag\n '\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398868","messageId":"20200529085038.26008-27-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 26/34] commit-graph: deduplicate modified path Bloom filters","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:30Z","receivedAt":"2020-05-29T08:51:53Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Some commits modify the same set of paths, and, consequently, have\nidentical modified path Bloom filters.\n\nUse a hashmap to find these identical Bloom filters, and omit all\nduplicates from the Modified Path Bloom Filters chunk, reducing its\nsize.\n\n                   MPBF chunk size\n                 without       with             Time spent\n                    deduplication                deduping\n  --------------------------------------------------------\n  android-base   8620198     2618764    -69.6%    0.089s\n  cmssw         10347018     9094468    -12.1%    0.056s\n  cpython         526381      371629    -29.4%    0.013s\n  elasticsearch  4733274     3607106    -23.8%    0.029s\n  gcc            3724544     3394521     -8.9%    0.072s\n  gecko-dev     20591010    17042732    -17.2%    0.235s\n  git             160718      139546    -13.2%    0.004s\n  glibc           759132      699623     -7.8%    0.009s\n  go              458375      421394     -8.1%    0.008s\n  jdk           13213208    12158533     -8.0%    0.034s\n  linux         26575278    25029700     -5.8%    0.169s\n  llvm-project   3988133     3269438    -18.0%    0.085s\n  rails           958691      614528    -35.9%    0.015s\n  rust           1985782     1682919    -15.3%    0.023s\n  tensorflow     4173570     3789768     -9.2%    0.022s\n  webkit         5891751     5394507     -8.4%    0.096s\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++----\n 1 file changed, 71 insertions(+), 5 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 56b8f33d10..178bbf0113 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -57,6 +57,7 @@\n \n struct modified_path_bloom_filter_info {\n \tstruct bloom_filter filter;\n+\tstruct modified_path_bloom_filter_info *duplicate_of;\n \t/*\n \t * The offset relative to the start of the Modified Path Bloom\n \t * Filters chunk where this Bloom filter has been written,\n@@ -1099,6 +1100,9 @@ struct write_commit_graph_context {\n \n \t\t/* Excluding embedded modified path Bloom filters */\n \t\tuint64_t total_filter_size;\n+\n+\t\t/* Used to find identical modified path Bloom filters */\n+\t\tstruct hashmap dedup_hashmap;\n \t} mpbfctx;\n };\n \n@@ -1315,7 +1319,11 @@ static int write_graph_chunk_modified_path_bloom_filters(struct hashfile *f,\n \t\tbfi = modified_path_bloom_filters_peek(\n \t\t\t\t&modified_path_bloom_filters, commit);\n \n-\t\tif (!bfi || !bfi->filter.nr_bits)\n+\t\tif (!bfi)\n+\t\t\tcontinue;\n+\t\tif (bfi->duplicate_of)\n+\t\t\tcontinue;\n+\t\tif (!bfi->filter.nr_bits)\n \t\t\tcontinue;\n \t\tif (bfi->filter.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS)\n \t\t\tcontinue;\n@@ -1364,6 +1372,9 @@ static int write_graph_chunk_modified_path_bloom_index(struct hashfile *f,\n \t\t\t */\n \t\t\tfilterdata[0] |= 1 << 7;\n \t\t\thashwrite(f, filterdata, sizeof(filterdata));\n+\t\t} else if (bfi->duplicate_of) {\n+\t\t\tuint64_t offset = htonll(bfi->duplicate_of->offset);\n+\t\t\thashwrite(f, &offset, sizeof(offset));\n \t\t} else if (bfi->offset != -1) {\n \t\t\tuint64_t offset = htonll(bfi->offset);\n \t\t\thashwrite(f, &offset, sizeof(offset));\n@@ -1515,6 +1526,53 @@ static void file_change_cb(struct diff_options *options,\n \thandle_modified_file(options->change_fn_data, fullpath);\n }\n \n+struct modified_path_bloom_filter_dedup_entry {\n+\tstruct hashmap_entry entry;\n+\tstruct modified_path_bloom_filter_info *bfi;\n+};\n+\n+static int modified_path_bloom_filter_dedup_cmp(const void *unused_cmp_data,\n+\t\tconst struct hashmap_entry *he1,\n+\t\tconst struct hashmap_entry *he2,\n+\t\tconst void *unused_keydata)\n+{\n+\tconst struct modified_path_bloom_filter_dedup_entry *a, *b;\n+\n+\ta = container_of(he1, const struct modified_path_bloom_filter_dedup_entry, entry);\n+\tb = container_of(he2, const struct modified_path_bloom_filter_dedup_entry, entry);\n+\n+\tif (a->bfi->filter.nr_bits != b->bfi->filter.nr_bits)\n+\t\treturn 1;\n+\n+\treturn memcmp(a->bfi->filter.bits, b->bfi->filter.bits,\n+\t\t      bloom_filter_bytes(&a->bfi->filter));\n+}\n+\n+static int handle_duplicate_modified_path_bloom_filter(\n+\t\tstruct modified_path_bloom_filter_context *mpbfctx,\n+\t\tstruct modified_path_bloom_filter_info *bfi)\n+{\n+\tstruct modified_path_bloom_filter_dedup_entry *e, stack_entry;\n+\tunsigned int h;\n+\n+\th = memhash(bfi->filter.bits, bloom_filter_bytes(&bfi->filter));\n+\thashmap_entry_init(&stack_entry.entry, h);\n+\tstack_entry.bfi = bfi;\n+\n+\te = hashmap_get_entry(&mpbfctx->dedup_hashmap, &stack_entry, entry,\n+\t\t\t      NULL);\n+\tif (e) {\n+\t\tbfi->duplicate_of = e->bfi;\n+\t\treturn 1;\n+\t} else {\n+\t\te = xmalloc(sizeof(*e));\n+\t\thashmap_entry_init(&e->entry, h);\n+\t\te->bfi = bfi;\n+\t\thashmap_add(&mpbfctx->dedup_hashmap, &e->entry);\n+\t\treturn 0;\n+\t}\n+}\n+\n static void create_modified_path_bloom_filter(\n \t\tstruct modified_path_bloom_filter_context *mpbfctx,\n \t\tstruct commit *commit)\n@@ -1547,17 +1605,20 @@ static void create_modified_path_bloom_filter(\n \tbfi = modified_path_bloom_filters_at(&modified_path_bloom_filters,\n \t\t\t\t\t     commit);\n \tbfi->offset = -1;\n-\tif (path_component_count > mpbfctx->embedded_limit) {\n+\tif (path_component_count > mpbfctx->embedded_limit)\n \t\tbloom_filter_init(&bfi->filter, mpbfctx->num_hashes,\n \t\t\t\t  path_component_count);\n-\t\tmpbfctx->total_filter_size += sizeof(uint32_t) +\n-\t\t\t\t\t      bloom_filter_bytes(&bfi->filter);\n-\t} else\n+\telse\n \t\tbloom_filter_init_with_size(&bfi->filter,\n \t\t\t\tGRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS);\n \n \tbloom_filter_set_bits(&bfi->filter, mpbfctx->hashes,\n \t\t\t      mpbfctx->hashes_nr);\n+\n+\tif (path_component_count > mpbfctx->embedded_limit &&\n+\t    !handle_duplicate_modified_path_bloom_filter(mpbfctx, bfi))\n+\t\tmpbfctx->total_filter_size += sizeof(uint32_t) +\n+\t\t\t\t\t      bloom_filter_bytes(&bfi->filter);\n }\n \n static void add_missing_parents(struct write_commit_graph_context *ctx, struct commit *commit)\n@@ -2396,6 +2457,8 @@ int write_commit_graph(const char *obj_dir,\n \t\tdiff_setup_done(&ctx->mpbfctx.diffopt);\n \n \t\tstrbuf_init(&ctx->mpbfctx.prev_path, PATH_MAX);\n+\t\thashmap_init(&ctx->mpbfctx.dedup_hashmap,\n+\t\t\t     modified_path_bloom_filter_dedup_cmp, NULL, 0);\n \t}\n \n \tif (ctx->split) {\n@@ -2525,6 +2588,9 @@ int write_commit_graph(const char *obj_dir,\n \t\tstrbuf_release(&ctx->mpbfctx.prev_path);\n \t\tfree(ctx->mpbfctx.hashed_prefix_lengths);\n \t\tfree(ctx->mpbfctx.hashes);\n+\t\thashmap_free_entries(&ctx->mpbfctx.dedup_hashmap,\n+\t\t\t\tstruct modified_path_bloom_filter_dedup_entry,\n+\t\t\t\tentry);\n \t}\n \n \tfree(ctx);\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398869","messageId":"20200529085038.26008-26-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 25/34] commit-graph: check embedded modified path Bloom filters with a mask","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:29Z","receivedAt":"2020-05-29T08:51:53Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Since the previous patch we check the presence of all leading\ndirectories of a pathspec in modified path Bloom filters to\nsignificantly lower the false positive rate.  This means that we are\nchecking a lot of bit positions in the Bloom filters.  However, as\nshown earlier in this series, a significant portion of commits have\nembedded modified path Bloom filters, all with the same size of 63\nbits.\n\nUse a 64 bit mask to check all relevant bits in embedded modified path\nBloom filters in one step.\n\nI don't have benchmark results at hand, but as far as I can recall\nsome old results this reduces the time spent loading and checking\nmodified path Bloom filters by up to 10%.  Checking Bloom filters is\nof course only a small part of the whole runtime of a pathspec-limited\nrevision walk, so the overall improvement is only about 1-2%.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 17 +++++++++++++++--\n pathspec.c     |  1 +\n pathspec.h     |  1 +\n 3 files changed, 17 insertions(+), 2 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 1fd1b4f8dd..56b8f33d10 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -918,10 +918,16 @@ enum bloom_result check_modified_path_bloom_filter(struct repository *r,\n \tfor (i = 0; i < pathspec->nr; i++) {\n \t\tstruct pathspec_item *pi = &pathspec->items[i];\n \n-\t\tif (bloom_filter_check_bits(&bf,\n+\t\tif (bf.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS) {\n+\t\t\t/* Gross!  And potential alignment issues?! */\n+\t\t\tif ((*((const uint64_t*)bf.bits) & pi->modified_path_bloom_mask) == pi->modified_path_bloom_mask)\n+\t\t\t\treturn BLOOM_POSSIBLY_YES;\n+\t\t} else {\n+\t\t\tif (bloom_filter_check_bits(&bf,\n \t\t\t\t\tpi->modified_path_bloom_hashes,\n \t\t\t\t\tpi->modified_path_bloom_hashes_nr))\n-\t\t\treturn BLOOM_POSSIBLY_YES;\n+\t\t\t\treturn BLOOM_POSSIBLY_YES;\n+\t\t}\n \t}\n \n \treturn BLOOM_DEFINITELY_NOT;\n@@ -981,6 +987,7 @@ void init_pathspec_bloom_fields(struct repository *r,\n \t\tsize_t len = pi->len;\n \t\tint path_component_nr = 0, j;\n \t\tuint32_t *hashes;\n+\t\tstruct bloom_filter embedded_bf;\n \n \t\t/*\n \t\t * Pathspec parsing has normalized away any consecutive\n@@ -1012,6 +1019,12 @@ void init_pathspec_bloom_fields(struct repository *r,\n \t\t\t\t\tgraph->num_modified_path_bloom_hashes,\n \t\t\t\t\thashes);\n \t\t}\n+\n+\t\tembedded_bf.nr_bits = GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS;\n+\t\tembedded_bf.bits = (uint8_t*) &pi->modified_path_bloom_mask;\n+\t\tbloom_filter_set_bits(&embedded_bf,\n+\t\t\t\t      pi->modified_path_bloom_hashes,\n+\t\t\t\t      pi->modified_path_bloom_hashes_nr);\n \t}\n \n \tpathspec->can_use_modified_path_bloom_filters = 1;\ndiff --git a/pathspec.c b/pathspec.c\nindex 01e7ae6944..29cadce013 100644\n--- a/pathspec.c\n+++ b/pathspec.c\n@@ -408,6 +408,7 @@ static void init_pathspec_item(struct pathspec_item *item, unsigned flags,\n \titem->attr_match_nr = 0;\n \titem->modified_path_bloom_hashes_nr = 0;\n \titem->modified_path_bloom_hashes = NULL;\n+\titem->modified_path_bloom_mask = 0;\n \n \t/* PATHSPEC_LITERAL_PATH ignores magic */\n \tif (flags & PATHSPEC_LITERAL_PATH) {\ndiff --git a/pathspec.h b/pathspec.h\nindex 0c661e1b03..c9975aea7a 100644\n--- a/pathspec.h\n+++ b/pathspec.h\n@@ -64,6 +64,7 @@ struct pathspec {\n \t\t */\n \t\tuint32_t modified_path_bloom_hashes_nr;\n \t\tuint32_t *modified_path_bloom_hashes;\n+\t\tuint64_t modified_path_bloom_mask;\n \t} *items;\n };\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398870","messageId":"20200529085038.26008-25-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 24/34] commit-graph: check all leading directories in modified path Bloom filters","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:28Z","receivedAt":"2020-05-29T08:51:55Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The file 'dir/subdir/file' can only be modified if its leading\ndirectories 'dir' and 'dir/subdir' are modified as well.\n\nSo when checking modified path Bloom filters looking for commits\nmodifying a path with multiple path components, then check not only\nthe full path in the Bloom filters, but all its leading directories as\nwell.  Take care to check these paths in \"deepest first\" order,\nbecause it's the full path that is least likely to be modified, and\nthe Bloom filter queries can short circuit sooner.\n\nThis can significantly reduce the average false positive rate, by\nabout an order of magnitude or three(!), and can further speed up\npathspec-limited revision walks.  The table below compares the average\nfalse positive rate and runtime of\n\n  git -c core.modifiedPathBloomFilters=1 rev-list HEAD -- \"$path\"\n\nbefore and after this change for 5000+ randomly selected paths from\neach repository:\n\n                    Average false           Average        Average\n                    positive rate           runtime        runtime\n                  before     after     before     after   difference\n  ------------------------------------------------------------------\n  android-base    0.526%   0.00416%    0.1727s   0.1503s   -13.0%\n  cmssw           0.494%   0.00395%    0.0426s   0.0332s   -22.0%\n  cpython         0.213%   0.01661%    0.0940s   0.0858s    -8.8%\n  elasticsearch   0.679%   0.00325%    0.0191s   0.0147s   -23.2%\n  gcc             0.398%   0.00892%    0.3423s   0.2192s   -36.0%\n  gecko-dev       0.472%   0.00073%    0.6243s   0.5134s   -17.8%\n  git             0.191%   0.06992%    0.0384s   0.0319s   -17.1%\n  glibc           0.392%   0.01639%    0.0425s   0.0296s   -30.5%\n  go              0.453%   0.01262%    0.0515s   0.0425s   -17.5%\n  jdk             0.662%   0.00643%    0.0083s   0.0070s   -15.0%\n  linux           0.434%   0.00749%    0.1102s   0.0911s   -17.3%\n  llvm-project    0.511%   0.00391%    0.4865s   0.4290s   -11.8%\n  rails           0.476%   0.01313%    0.0464s   0.0410s   -11.6%\n  rust            0.457%   0.02551%    0.0590s   0.0462s   -21.7%\n  tensorflow      0.511%   0.00824%    0.0642s   0.0517s   -19.5%\n  webkit          0.661%   0.00101%    0.3315s   0.2576s   -22.3%\n\nThe improvements in runtime are much smaller than the improvements in\naverage false positive rate, as we are clearly reaching diminishing\nreturns here.  However, all these timings depend on that accessing\ntree objects is reasonably fast (warm caches).  If we had a partial\nclone and the tree objects had to be fetched from a promisor remote,\ne.g.:\n\n  $ git clone --filter=tree:0 --bare file://.../webkit.git webkit.notrees.git\n  $ git -C webkit.git -c core.modifiedPathBloomFilters=1 \\\n        commit-graph write --reachable\n  $ cp webkit.git/objects/info/commit-graph webkit.notrees.git/objects/info/\n  $ git -C webkit.notrees.git -c core.modifiedPathBloomFilters=1 \\\n        rev-list HEAD -- \"$path\"\n\nthen checking all leading path component can reduce the runtime from\nover an hour to a few seconds (and this is with the clone and the\npromisor on the same machine).\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 27 ++++++++++++++++++++++-----\n 1 file changed, 22 insertions(+), 5 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex f9a21ecdfb..1fd1b4f8dd 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -977,8 +977,10 @@ void init_pathspec_bloom_fields(struct repository *r,\n \n \tfor (i = 0; i < pathspec->nr; i++) {\n \t\tstruct pathspec_item *pi = &pathspec->items[i];\n-\t\tconst char *path = pi->match;\n+\t\tconst char *path = pi->match, *p;\n \t\tsize_t len = pi->len;\n+\t\tint path_component_nr = 0, j;\n+\t\tuint32_t *hashes;\n \n \t\t/*\n \t\t * Pathspec parsing has normalized away any consecutive\n@@ -988,13 +990,28 @@ void init_pathspec_bloom_fields(struct repository *r,\n \t\tif (path[len - 1] == '/')\n \t\t\tlen--;\n \n-\t\tpi->modified_path_bloom_hashes_nr = graph->num_modified_path_bloom_hashes;\n+\t\tp = path;\n+\t\tdo {\n+\t\t\tp = strchrnul(p + 1, '/');\n+\t\t\tpath_component_nr++;\n+\t\t} while (p - path < len);\n+\n+\t\tpi->modified_path_bloom_hashes_nr = path_component_nr * graph->num_modified_path_bloom_hashes;\n \t\tALLOC_ARRAY(pi->modified_path_bloom_hashes,\n \t\t\t    pi->modified_path_bloom_hashes_nr);\n \n-\t\tcompute_modified_path_bloom_hashes_for_path(path, len,\n-\t\t\t\tgraph->num_modified_path_bloom_hashes,\n-\t\t\t\tpi->modified_path_bloom_hashes);\n+\t\tp = path;\n+\t\thashes = pi->modified_path_bloom_hashes +\n+\t\t\t pi->modified_path_bloom_hashes_nr;\n+\t\tfor (j = 0; j < path_component_nr; j++) {\n+\t\t\tp = strchrnul(p + 1, '/');\n+\n+\t\t\thashes -= graph->num_modified_path_bloom_hashes;\n+\t\t\tcompute_modified_path_bloom_hashes_for_path(path,\n+\t\t\t\t\tp - path,\n+\t\t\t\t\tgraph->num_modified_path_bloom_hashes,\n+\t\t\t\t\thashes);\n+\t\t}\n \t}\n \n \tpathspec->can_use_modified_path_bloom_filters = 1;\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398871","messageId":"20200529085038.26008-24-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 23/34] commit-graph: load and use the Modified Path Bloom Filters chunk","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:27Z","receivedAt":"2020-05-29T08:51:57Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"The table below compares the average runtime and memory usage of\n\n  git rev-list HEAD -- \"$path\"\n\nwith and without modified path Bloom filters for 5000+ randomly\nselected paths from each repository:\n\n                  Average runtime            |       Max RSS\n                 without     with   Speedup  |  without   with\n  -------------------------------------------+-------------------------\n  android-base   0.8780s   0.1742s    5.04x  |   387MB   227MB   -41.4%\n  cmssw          0.3143s   0.0452s    6.95x  |   181MB    79MB   -56.4%\n  cpython        0.7453s   0.0956s    7.80x  |   159MB    71MB   -55.4%\n  elasticsearch  0.1492s   0.0191s    7.82x  |   134MB    53MB   -60.2%\n  gcc            7.1852s   0.3584s   20.05x  |   297MB   231MB   -22.3%\n  gecko-dev      4.6113s   0.6318s    7.30x  |   832MB   600MB   -27.9%\n  git            0.6180s   0.0405s   15.26x  |   131MB    43MB   -67.2%\n  glibc          0.5618s   0.0471s   11.93x  |   136MB    50MB   -63.3%\n  go             0.4913s   0.0515s    9.53x  |   130MB    50MB   -61.5%\n  jdk            0.0482s   0.0089s    5.42x  |    52MB    39MB   -25.0%\n  linux          0.7043s   0.1129s    6.24x  |   438MB   270MB   -38.4%\n  llvm-project   2.6844s   0.4873s    5.51x  |   384MB   282MB   -26.6%\n  rails          0.2784s   0.0484s    5.75x  |    88MB    51MB   -42.1%\n  rust           0.7757s   0.0619s   12.53x  |   345MB    89MB   -74.3%\n  tensorflow     0.6258s   0.0642s    9.74x  |   233MB    85MB   -63.5%\n  webkit         1.9137s   0.3332s    5.74x  |   941MB   480MB   -49.0%\n---\n commit-graph.c | 44 ++++++++++++++++++++++++++++++++++++++++++--\n commit-graph.h |  2 ++\n 2 files changed, 44 insertions(+), 2 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 3210ec2f93..f9a21ecdfb 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -327,6 +327,15 @@ struct commit_graph *parse_commit_graph(void *graph_map, int fd,\n \t\t\t}\n \t\t\tbreak;\n \n+\t\tcase GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTERS:\n+\t\t\tif (graph->chunk_mpbf_filters)\n+\t\t\t\tchunk_repeated = 1;\n+\t\t\telse {\n+\t\t\t\tgraph->chunk_mpbf_filters = data + chunk_offset;\n+\t\t\t\tgraph->chunk_mpbf_filters_size = next_chunk_offset - chunk_offset;\n+\t\t\t}\n+\t\t\tbreak;\n+\n \t\tcase GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_EXCLUDES:\n \t\t\tif (graph->chunk_mpbf_excludes)\n \t\t\t\tchunk_repeated = 1;\n@@ -830,6 +839,7 @@ static int load_modified_path_bloom_filter_from_graph(\n \t\tstruct commit *parent, struct bloom_filter *bf)\n {\n \tconst uint8_t *bloom_index;\n+\tuint64_t offset;\n \tint first_parent = 0;\n \n \tif (commit->graph_pos == COMMIT_NOT_FROM_GRAPH)\n@@ -857,9 +867,39 @@ static int load_modified_path_bloom_filter_from_graph(\n \t\tbf->nr_bits = GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS;\n \t\tbf->bits = (uint8_t*) bloom_index;\n \t\treturn 1;\n+\t} else if (bloom_index[0] & (1 << 6)) {\n+\t\t/*\n+\t\t * Modified path Bloom filters for second..nth parents of\n+\t\t * merge commits are not implemented yet.\n+\t\t */\n+\t\treturn 0;\n+\t} else {\n+\t\tif (!first_parent)\n+\t\t\treturn 0;\n+\t\toffset = get_be64(bloom_index);\n \t}\n-\t/* support for non-embedded Bloom filters is not implemented yet. */\n-\treturn 0;\n+\n+\tif (!graph->chunk_mpbf_filters)\n+\t\tBUG(\"commit %s refers to offset %lu of the Modified Path Bloom Filters chunk, but that chunk is missing\",\n+\t\t    oid_to_hex(&commit->object.oid), offset);\n+\n+\tif (offset + sizeof(uint32_t) >= graph->chunk_mpbf_filters_size)\n+\t\tBUG(\"commit %s refers to offset %lu of the Modified Path Bloom Filters chunk, but that's too large for chunk of size %lu bytes\",\n+\t\t    oid_to_hex(&commit->object.oid), offset,\n+\t\t    graph->chunk_mpbf_filters_size);\n+\n+\tbf->nr_bits = get_be32(graph->chunk_mpbf_filters + offset);\n+\tif (!bf->nr_bits)\n+\t\tBUG(\"commit %s has a modified path Bloom filter at offset %lu, which has zero size\",\n+\t\t    oid_to_hex(&commit->object.oid), offset);\n+\tif (offset + sizeof(uint32_t) + bloom_filter_bytes(bf) > graph->chunk_mpbf_filters_size)\n+\t\tBUG(\"commit %s has a modified path Bloom filter of %u bits at offset %lu, which doesn't fit into a Modified Path Bloom Filters chunk of %lu bytes\",\n+\t\t    oid_to_hex(&commit->object.oid), bf->nr_bits, offset,\n+\t\t    graph->chunk_mpbf_filters_size);\n+\t/* Casting away const-ness :( */\n+\tbf->bits = (uint8_t*)(graph->chunk_mpbf_filters + offset + sizeof(uint32_t));\n+\n+\treturn 1;\n }\n \n enum bloom_result check_modified_path_bloom_filter(struct repository *r,\ndiff --git a/commit-graph.h b/commit-graph.h\nindex 09dfc16932..cde0d7fa30 100644\n--- a/commit-graph.h\n+++ b/commit-graph.h\n@@ -69,6 +69,8 @@ struct commit_graph {\n \tconst unsigned char *chunk_base_graphs;\n \tconst unsigned char *chunk_mpbf_index;\n \tuint64_t chunk_mpbf_index_size;\n+\tconst unsigned char *chunk_mpbf_filters;\n+\tuint64_t chunk_mpbf_filters_size;\n \tconst unsigned char *chunk_mpbf_excludes;\n \n \tuint8_t num_modified_path_bloom_hashes;\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398872","messageId":"20200529085038.26008-23-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 22/34] commit-graph: write the Modified Path Bloom Filters chunk","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:26Z","receivedAt":"2020-05-29T08:51:59Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Write the Modified Path Bloom Filters chunk first, keeping track of\nthe offsets where each (non-embedded) Bloom filter has been written,\nand then write the Modified Path Bloom Filter Index chunk using those\nrecorded offsets.\n---\n commit-graph.c | 78 ++++++++++++++++++++++++++++++++++++++++++++------\n 1 file changed, 69 insertions(+), 9 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex fb24600bb3..3210ec2f93 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -26,6 +26,7 @@\n #define GRAPH_CHUNKID_EXTRAEDGES 0x45444745 /* \"EDGE\" */\n #define GRAPH_CHUNKID_BASE 0x42415345 /* \"BASE\" */\n #define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX 0x4d504249 /* \"MPBI\" */\n+#define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTERS 0x4d504246 /* \"MPBF\" */\n #define GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_EXCLUDES 0x4d504258 /* \"MPBX\" */\n \n #define GRAPH_DATA_WIDTH (the_hash_algo->rawsz + 16)\n@@ -56,6 +57,12 @@\n \n struct modified_path_bloom_filter_info {\n \tstruct bloom_filter filter;\n+\t/*\n+\t * The offset relative to the start of the Modified Path Bloom\n+\t * Filters chunk where this Bloom filter has been written,\n+\t * -1 before that.\n+\t */\n+\tuint64_t offset;\n };\n \n static void free_modified_path_bloom_filter_info_in_slab(\n@@ -1019,6 +1026,9 @@ struct write_commit_graph_context {\n \t\t */\n \t\tuint32_t *hashes;\n \t\tint hashes_nr, hashes_alloc;\n+\n+\t\t/* Excluding embedded modified path Bloom filters */\n+\t\tuint64_t total_filter_size;\n \t} mpbfctx;\n };\n \n@@ -1219,6 +1229,43 @@ static int write_graph_chunk_extra_edges(struct hashfile *f,\n \treturn 0;\n }\n \n+static int write_graph_chunk_modified_path_bloom_filters(struct hashfile *f,\n+\t\tstruct write_commit_graph_context *ctx)\n+{\n+\tuint64_t offset = 0;\n+\tint i;\n+\n+\tfor (i = 0; i < ctx->commits.nr; i++) {\n+\t\tstruct commit *commit = ctx->commits.list[i];\n+\t\tstruct modified_path_bloom_filter_info *bfi;\n+\t\tunsigned int filter_size;\n+\n+\t\tdisplay_progress(ctx->progress, ++ctx->progress_cnt);\n+\n+\t\tbfi = modified_path_bloom_filters_peek(\n+\t\t\t\t&modified_path_bloom_filters, commit);\n+\n+\t\tif (!bfi || !bfi->filter.nr_bits)\n+\t\t\tcontinue;\n+\t\tif (bfi->filter.nr_bits == GRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS)\n+\t\t\tcontinue;\n+\n+\t\tif (offset >> 62)\n+\t\t\tBUG(\"offset %lu is too large for the Modified Path Bloom Filter Index chunk\",\n+\t\t\t    offset);\n+\n+\t\tbfi->offset = offset;\n+\n+\t\tfilter_size = bloom_filter_bytes(&bfi->filter);\n+\n+\t\thashwrite_be32(f, bfi->filter.nr_bits);\n+\t\thashwrite(f, bfi->filter.bits, filter_size);\n+\n+\t\toffset += sizeof(uint32_t) + filter_size;\n+\t}\n+\treturn 0;\n+}\n+\n static int write_graph_chunk_modified_path_bloom_index(struct hashfile *f,\n \t\tstruct write_commit_graph_context *ctx)\n {\n@@ -1247,8 +1294,11 @@ static int write_graph_chunk_modified_path_bloom_index(struct hashfile *f,\n \t\t\t */\n \t\t\tfilterdata[0] |= 1 << 7;\n \t\t\thashwrite(f, filterdata, sizeof(filterdata));\n+\t\t} else if (bfi->offset != -1) {\n+\t\t\tuint64_t offset = htonll(bfi->offset);\n+\t\t\thashwrite(f, &offset, sizeof(offset));\n \t\t} else\n-\t\t\tBUG(\"writing non-embedded Bloom filters is not implemented yet\");\n+\t\t\tBUG(\"modified path Bloom filter offset is still -1?!\");\n \t}\n \treturn 0;\n }\n@@ -1424,17 +1474,20 @@ static void create_modified_path_bloom_filter(\n \tdiff_tree_oid(parent_oid, &commit->object.oid, \"\", &mpbfctx->diffopt);\n \tpath_component_count = mpbfctx->hashes_nr / mpbfctx->num_hashes;\n \n+\tbfi = modified_path_bloom_filters_at(&modified_path_bloom_filters,\n+\t\t\t\t\t     commit);\n+\tbfi->offset = -1;\n \tif (path_component_count > mpbfctx->embedded_limit) {\n-\t\t/* Not implemented yet. */\n-\t} else {\n-\t\tbfi = modified_path_bloom_filters_at(\n-\t\t\t\t&modified_path_bloom_filters, commit);\n-\n+\t\tbloom_filter_init(&bfi->filter, mpbfctx->num_hashes,\n+\t\t\t\t  path_component_count);\n+\t\tmpbfctx->total_filter_size += sizeof(uint32_t) +\n+\t\t\t\t\t      bloom_filter_bytes(&bfi->filter);\n+\t} else\n \t\tbloom_filter_init_with_size(&bfi->filter,\n \t\t\t\tGRAPH_MODIFIED_PATH_BLOOM_FILTER_EMBEDDED_NR_BITS);\n-\t\tbloom_filter_set_bits(&bfi->filter, mpbfctx->hashes,\n-\t\t\t\t      mpbfctx->hashes_nr);\n-\t}\n+\n+\tbloom_filter_set_bits(&bfi->filter, mpbfctx->hashes,\n+\t\t\t      mpbfctx->hashes_nr);\n }\n \n static void add_missing_parents(struct write_commit_graph_context *ctx, struct commit *commit)\n@@ -1845,6 +1898,13 @@ static int write_commit_graph_file(struct write_commit_graph_context *ctx)\n \t\tchunks_nr++;\n \t}\n \tif (ctx->mpbfctx.use_modified_path_bloom_filters) {\n+\t\tif (ctx->mpbfctx.total_filter_size) {\n+\t\t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n+\t\t\tchunks[chunks_nr].id = GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTERS;\n+\t\t\tchunks[chunks_nr].size = ctx->mpbfctx.total_filter_size;\n+\t\t\tchunks[chunks_nr].write_fn = write_graph_chunk_modified_path_bloom_filters;\n+\t\t\tchunks_nr++;\n+\t\t}\n \t\tALLOC_GROW(chunks, chunks_nr + 1, chunks_alloc);\n \t\tchunks[chunks_nr].id = GRAPH_CHUNKID_MODIFIED_PATH_BLOOM_FILTER_INDEX;\n \t\tchunks[chunks_nr].size = sizeof(uint8_t) +\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398873","messageId":"20200529085038.26008-35-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 34/34] commit-graph: use modified path Bloom filters with wildcards, if possible","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:38Z","receivedAt":"2020-05-29T08:52:00Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"Modified path Bloom filter don't store the names of modified paths,\nthey only set a couple of bits based on those paths' hashes.\nConsequently, they can only be used when looking for the history of a\nconcrete path, so we disabled them when looking for pathspecs with\nwildcards.\n\nHowever, if the pathspec has \"wildcard-less\" leading directories, then\nwe can use modified path Bloom filters to skip commits that don't\nmodify those leading directories.  As a result, something like:\n\n  git -c core.modifiedPathBloomFilters=1 rev-list HEAD -- 'compat/win32/pthread.*'\n\nwill take only ~0.045s instead of ~1.24s, achieving over 27x speedup.\n\nFor comparison, letting the shell do the wildcard matching, i.e. the\nequivalent of using the pathspecs 'compat/win32/pthread.c' and\n'compat/win32/pthread.h' takes ~0.311s without using modified path\nBloom filters: apparently tree-diff with wildcards can be considerably\nmore expensive that without wildcards, even if the wildcard is in the\nlast path component and that directory only contains a dozen files.\n\nSigned-off-by: SZEDER Gábor <szeder.dev@gmail.com>\n---\n commit-graph.c | 44 ++++++++++++++++++++++++++++++++++----------\n 1 file changed, 34 insertions(+), 10 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 8eb0cbedaf..db43877426 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1023,9 +1023,9 @@ static void compute_modified_path_bloom_hashes_for_path(const char *path,\n void init_pathspec_bloom_fields(struct repository *r,\n \t\t\t\tstruct pathspec *pathspec)\n {\n-\tconst unsigned bloom_compatible_magic = PATHSPEC_LITERAL;\n+\tconst unsigned bloom_compatible_magic = PATHSPEC_LITERAL | PATHSPEC_GLOB;\n \tstruct commit_graph *graph = r->objects->commit_graph;\n-\tint i;\n+\tint i, can_use_modified_path_bloom_filters;\n \n \tif (!graph)\n \t\treturn;\n@@ -1033,15 +1033,14 @@ void init_pathspec_bloom_fields(struct repository *r,\n \t\treturn;\n \tif (!pathspec->nr)\n \t\treturn;\n-\tif (pathspec->has_wildcard)\n-\t\treturn;\n \tif (pathspec->magic & ~bloom_compatible_magic)\n \t\treturn;\n \n+\tcan_use_modified_path_bloom_filters = 1;\n \tfor (i = 0; i < pathspec->nr; i++) {\n \t\tstruct pathspec_item *pi = &pathspec->items[i];\n \t\tconst char *path = pi->match, *p;\n-\t\tsize_t len = pi->len;\n+\t\tsize_t nowildcard_len = pi->nowildcard_len;\n \t\tint path_component_nr = 0, j;\n \t\tuint32_t *hashes;\n \t\tstruct bloom_filter embedded_bf;\n@@ -1051,14 +1050,29 @@ void init_pathspec_bloom_fields(struct repository *r,\n \t\t * slashes, but a trailing slash might still be present,\n \t\t * \"remove\" it.\n \t\t */\n-\t\tif (path[len - 1] == '/')\n-\t\t\tlen--;\n+\t\tif (path[nowildcard_len - 1] == '/')\n+\t\t\tnowildcard_len--;\n \n \t\tp = path;\n \t\tdo {\n \t\t\tp = strchrnul(p + 1, '/');\n-\t\t\tpath_component_nr++;\n-\t\t} while (p - path < len);\n+\t\t\tif (p - path <= nowildcard_len)\n+\t\t\t\tpath_component_nr++;\n+\t\t} while (p - path < nowildcard_len);\n+\t\t/*\n+\t\t * If a pathspec uses wildcards but has wildcard-less\n+\t\t * leading directories, then we can use modified path Bloom\n+\t\t * filters to skip commits that don't modify those leading\n+\t\t * directories.\n+\t\t * However, if there is even one pathspec that has a wilcard\n+\t\t * in its first path component, then we have no choice but\n+\t\t * to run tree-diff anyway, so don't bother with Bloom\n+\t\t * filters at all in that case.\n+\t\t */\n+\t\tif (!path_component_nr) {\n+\t\t\tcan_use_modified_path_bloom_filters = 0;\n+\t\t\tbreak;\n+\t\t}\n \n \t\tpi->modified_path_bloom_hashes_nr = path_component_nr * graph->num_modified_path_bloom_hashes;\n \t\tALLOC_ARRAY(pi->modified_path_bloom_hashes,\n@@ -1084,7 +1098,17 @@ void init_pathspec_bloom_fields(struct repository *r,\n \t\t\t\t      pi->modified_path_bloom_hashes_nr);\n \t}\n \n-\tpathspec->can_use_modified_path_bloom_filters = 1;\n+\tif (can_use_modified_path_bloom_filters) {\n+\t\tpathspec->can_use_modified_path_bloom_filters = 1;\n+\t} else {\n+\t\tint j;\n+\t\tfor (j = 0; j < i; j++) {\n+\t\t\tstruct pathspec_item *pi = &pathspec->items[j];\n+\t\t\tFREE_AND_NULL(pi->modified_path_bloom_hashes);\n+\t\t\tpi->modified_path_bloom_hashes_nr = 0;\n+\t\t\tpi->modified_path_bloom_mask = 0;\n+\t\t}\n+\t}\n }\n \n struct packed_commit_list {\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398874","messageId":"20200529085038.26008-34-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 33/34] commit-graph: write modified path Bloom filters in \"history order\"","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:37Z","receivedAt":"2020-05-29T08:52:01Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"[Explain!  Try topo order!]\n\nI don't have recent benchmark results at hand yet, but as far as I can\nrecall some old results this reduces the time spent loading and\nchecking modified path Bloom filters by another ~10%.  Checking Bloom\nfilters is of course only a small part of the whole runtime of a\npathspec-limited revision walk, so the overall improvement is only\nabout 1-2%.\nOh, well, I expected more from this change... But perhaps it has\nlarger impact with less embedded modified path Bloom filters?\n\nThis is the last patch in this series that improves pathspec-limited\nrevision walk performance, so it's time to compare average runtime and\nmemory usage of\n\n  git rev-list HEAD -- \"$path\"\n\nfor 5000+ randomly selected paths from each repository with and\nwithout modified path Bloom filters:\n\n                  Average runtime     Average  |  Average max RSS\n                 without      with    speedup  |  without    with\n  ---------------------------------------------+----------------------------\n  android-base   0.8780s    0.1456s     6.03x  |  387MB    91.2MB    -76.5%\n  cmssw          0.3143s    0.0313s    10.04x  |  181MB    37.4MB    -79.4%\n  cpython        0.7453s    0.0810s     9.20x  |  159MB    43.8MB    -72.5%\n  elasticsearch  0.1492s    0.0136s    10.95x  |  134MB    21.8MB    -83.7%\n  gcc            7.1852s    0.2114s    34.00x  |  297MB    91.1MB    -69.3%\n  gecko-dev      4.6113s    0.4815s     9.58x  |  832MB   175.2MB    -79.0%\n  git            0.6180s    0.0310s    19.94x  |  131MB    31.5MB    -76.0%\n  glibc          0.5618s    0.0282s    19.92x  |  135MB    25.0MB    -81.6%\n  go             0.4913s    0.0403s    12.19x  |  130MB    24.7MB    -81.1%\n  jdk            0.0482s    0.0068s     7.09x  |   52MB    27.1MB    -48.2%\n  linux          0.7043s    0.0873s     8.08x  |  438MB   150.9MB    -65.5%\n  llvm-project   2.6844s    0.4135s     6.49x  |  384MB   126.5MB    -67.1%\n  rails          0.2784s    0.0391s     7.12x  |   88MB    28.8MB    -67.3%\n  rust           0.7757s    0.0439s    17.67x  |  344MB    42.0MB    -87.8%\n  tensorflow     0.6258s    0.0487s    12.85x  |  233MB    36.7MB    -84.2%\n  webkit         1.9137s    0.2420s     7.91x  |  940MB    84.7MB    -91.0%\n\nThe astute reader may notice that in several cases the achieved\nspeedup is a bit higher than the calculated potential speedup shown in\nthe log message of the patch adding the modified path Bloom filter\nspecs, e.g. 34x vs. 29.7x in the gcc repository.  I suspect that by\neliminating much of the tree-diff overhead we load a lot less (tree)\nobjects, putting less strain on the caches, and in turn making other\nparts of the process faster as well.\n\nAlas, this is not without extra cost: writing the commit-graph file\nwith modified path Bloom filters takes a lot longer and the resulting\ncommit-graph file is considerably larger:\n\n          Writing a commit-graph file from      |\n             scratch with '--reachable'         | commit-graph file size\n                 without       with             |   without      with\n  ----------------------------------------------+--------------------------------\n  android-base    6.230s     40.880s     6.56x  |  25077512    31278605    +24.7%\n  cmssw           3.177s     25.691s     8.09x  |  13017516    23971497    +84.1%\n  cpython         1.344s      8.951s     6.66x  |   6808068     8152146    +19.7%\n  gcc             2.886s     36.917s    12.79x  |  12839660    18068286    +40.7%\n  gecko-dev       9.331s     97.729s    10.47x  |  43335208    66568549    +53.6%\n  git             0.750s      5.245s     6.99x  |   3388208     4011595    +18.3%\n  glibc           0.565s      4.146s     7.34x  |   2832460     3936588    +38.9%\n  homebrew-cask   1.083s     27.024s    24.95x  |   5928644     6859160    +15.7%\n  homebrew-core   1.702s     55.478s    32.60x  |   9171324    10509040    +14.5%\n  jdk             0.568s     19.418s    34.19x  |   3237508    15858410   +389.8%\n  linux          13.525s    100.837s     7.46x  |  49800744    81939893    +64.5%\n  llvm-project    4.275s     31.188s     7.30x  |  19309116    25336867    +31.2%\n  rails           0.986s      5.607s     5.69x  |   4990252     6317541    +26.5%\n  rust            1.260s     13.250s    10.52x  |   6053824     8601440    +42.0%\n  webkit          3.658s     30.469s     8.33x  |  12288620    19438512    +58.1%\n---\n commit-graph.c | 28 +++++++++++++++++++++-------\n 1 file changed, 21 insertions(+), 7 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 1677e4fb3e..8eb0cbedaf 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -67,6 +67,8 @@ struct modified_path_bloom_filter_info {\n \tuint64_t offset;\n \tuint32_t merge_index_pos;\n \tstruct modified_path_bloom_filter_info *next;\n+\t/* TODO: need better names for 'next' and 'next_commit_bfi' */\n+\tstruct modified_path_bloom_filter_info *next_commit_bfi;\n };\n \n static void free_modified_path_bloom_filter_info_in_slab(\n@@ -1159,6 +1161,10 @@ struct write_commit_graph_context {\n \n \t\t/* Used to find identical modified path Bloom filters */\n \t\tstruct hashmap dedup_hashmap;\n+\n+\t\t/* To chain up Bloom filters in history order */\n+\t\tstruct modified_path_bloom_filter_info *first_commit_bfi;\n+\t\tstruct modified_path_bloom_filter_info *prev_commit_bfi;\n \t} mpbfctx;\n };\n \n@@ -1363,18 +1369,18 @@ static int write_graph_chunk_modified_path_bloom_filters(struct hashfile *f,\n \t\tstruct write_commit_graph_context *ctx)\n {\n \tuint64_t offset = 0;\n-\tint i;\n+\tstruct modified_path_bloom_filter_info *next_commit_bfi;\n \n-\tfor (i = 0; i < ctx->commits.nr; i++) {\n-\t\tstruct commit *commit = ctx->commits.list[i];\n-\t\tstruct modified_path_bloom_filter_info *bfi;\n-\t\tunsigned int filter_size;\n+\tnext_commit_bfi = ctx->mpbfctx.first_commit_bfi;\n+\twhile (next_commit_bfi) {\n+\t\tstruct modified_path_bloom_filter_info *bfi = next_commit_bfi;\n+\n+\t\tnext_commit_bfi = next_commit_bfi->next_commit_bfi;\n \n \t\tdisplay_progress(ctx->progress, ++ctx->progress_cnt);\n \n-\t\tbfi = modified_path_bloom_filters_peek(\n-\t\t\t\t&modified_path_bloom_filters, commit);\n \t\tfor (; bfi; bfi = bfi->next) {\n+\t\t\tunsigned int filter_size;\n \t\t\tif (bfi->duplicate_of)\n \t\t\t\tcontinue;\n \t\t\tif (!bfi->filter.nr_bits)\n@@ -1762,6 +1768,14 @@ static void create_modified_path_bloom_filter(\n \tbloom_filter_set_bits(&bfi->filter, mpbfctx->hashes,\n \t\t\t      mpbfctx->hashes_nr);\n \n+\tif (!mpbfctx->first_commit_bfi) {\n+\t\tmpbfctx->first_commit_bfi = bfi;\n+\t\tmpbfctx->prev_commit_bfi = bfi;\n+\t} else if (!nth_parent) {\n+\t\tmpbfctx->prev_commit_bfi->next_commit_bfi = bfi;\n+\t\tmpbfctx->prev_commit_bfi = bfi;\n+\t}\n+\n \tif (path_component_count > mpbfctx->embedded_limit &&\n \t    !handle_duplicate_modified_path_bloom_filter(mpbfctx, bfi))\n \t\tmpbfctx->total_filter_size += sizeof(uint32_t) +\n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398875","messageId":"20200529085038.26008-33-szeder.dev@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"[PATCH 32/34] PoC commit-graph: use revision walk machinery for '--reachable'","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-05-29T08:50:36Z","receivedAt":"2020-05-29T08:52:02Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"[Not sure it is legal to do this.  I hope it's legal.  It would be\ngreat if it were legal.  But I really doubt it's legal.  FWIW, at\nleast the test suite still passes, even with GIT_TEST_COMMIT_GRAPH=1.\nOr we have a hole in test coverage...]\n\nOur revision walk machinery knows a thing or two about how to traverse\nthe history in an order that is friendlier to the delta base cache.\nThe hand-rolled revision walk in commit-graph.c:close_reachable() is\nunaware of any such tricks: it is just a simple loop iterating over an\narray of commits, appending unseen parents to the end.  This didn't\nreally matter in the past, because we only accessed commit objects\nwhile writing the commit-graph.  Now, however, we have modified path\nBloom filters, and to fill them we need access to tree objects as well\nto gather all modified paths, which puts more strain on the caches,\nand the order in which we traverse the commits becomes relevant.\n\nSo let's use the regular revision walk machinery (i.e. 'struct\nrev_info', setup_revisions(), get_revision() and friends) for\n'--reachable' to speed up writing a commit-graph with modified path\nBloom filters.\n\nHowever, stick to the current algorithm when scanning pack files in\ncommits, because passing all packed commits to setup_revisions() might\ndo more harm than good (presumably, I didn't check; scanning all packs\nfor commits can be hopelessly inefficient anyway).  '--stdin-commits',\ntoo, could benefit from using the revision walk machinery when it's\nfed with branch tips from 'git for-each-ref', or fare worse when it's\nfed all commits from 'git rev-list' (didn't check this, either)... so\nlet's just leave it using the current algorithm for now.\n\nThe table below shows the reduced runtime and max RSS of writing a\ncommit-graph file from scratch with '--reachable' and modified path\nBloom filters, i.e. that of:\n\n  git -c core.modifiedPathBloomFilters=1 commit-graph write --reachable\n\n                       Runtime                         Max RSS\n                  before     after               before      after\n  --------------------------------------------------------------------------\n  android-base    57.722s   40.113s   -30.5%     711804k    697624k    -2.0%\n  cmssw           27.977s   25.154s   -10.1%     980884k    786196k   -19.9%\n  cpython         10.161s    8.956s   -11.9%     331736k    328704k    -0.9%\n  gcc            114.980s   36.975s   -67.9%     561928k    484212k   -13.8%\n  gecko-dev      132.557s   97.314s   -26.6%    2203544k   2161480k    -1.9%\n  git              8.097s    5.215s   -35.6%     240564k    189652k   -21.2%\n  glibc            4.693s    3.996s   -14.9%     215924k    207572k    -3.9%\n  homebrew-core   56.661s   56.384s    -0.5%     469668k    475596k    +1.3%\n  jdk             20.355s   18.936s    -7.0%     327244k    322316k    -1.5%\n  linux          215.235s   98.991s   -54.0%    1549852k   1445028k    -6.8%\n  llvm-project    48.659s   30.933s   -36.4%     783740k    736988k    -6.0%\n  rails            5.804s    5.389s    -7.2%     284524k    284528k       0%\n  rust            20.151s   12.921s   -35.9%     687840k    664104k    -3.5%\n  webkit          31.226s   30.341s    -2.8%    2018396k   1902884k    -5.7%\n---\n commit-graph.c | 110 ++++++++++++++++++++++++++++++++++++++++++-------\n 1 file changed, 94 insertions(+), 16 deletions(-)\n\ndiff --git a/commit-graph.c b/commit-graph.c\nindex 4cbfce61bf..1677e4fb3e 100644\n--- a/commit-graph.c\n+++ b/commit-graph.c\n@@ -1703,6 +1703,9 @@ static void create_modified_path_bloom_filter(\n \t * (a commit can be present in multiple pack files, or multiple\n \t * refs can point to the same commit) so check first whether we have\n \t * already created a modified path Bloom filter for it.\n+\t *\n+\t * TODO: with '--reachable' we can now be sure that we only look at\n+\t * each commit only once, so this is unnecessary in that code path.\n \t */\n \tbfi = modified_path_bloom_filters_peek(&modified_path_bloom_filters,\n \t\t\t\t\t       commit);\n@@ -1891,16 +1894,6 @@ static void compute_generation_numbers(struct write_commit_graph_context *ctx)\n \tstop_progress(&ctx->progress);\n }\n \n-static int add_ref_to_list(const char *refname,\n-\t\t\t   const struct object_id *oid,\n-\t\t\t   int flags, void *cb_data)\n-{\n-\tstruct string_list *list = (struct string_list *)cb_data;\n-\n-\tstring_list_append(list, oid_to_hex(oid));\n-\treturn 0;\n-}\n-\n static int fill_oids_from_packs(struct write_commit_graph_context *ctx,\n \t\t\t\tstruct string_list *pack_indexes)\n {\n@@ -2773,14 +2766,99 @@ int write_commit_graph_reachable(const char *obj_dir,\n \t\t\t\t enum commit_graph_write_flags flags,\n \t\t\t\t const struct split_commit_graph_opts *split_opts)\n {\n-\tstruct string_list list = STRING_LIST_INIT_DUP;\n-\tint result;\n+\tstruct write_commit_graph_context *ctx;\n+\tstruct rev_info revs;\n+\tconst char *revs_argv[] = { NULL, \"--all\", NULL };\n+\tstruct commit *commit;\n+\tint result = 0;\n+\n+\tctx = init_write_commit_graph_context(obj_dir, flags, split_opts);\n+\tif (!ctx)\n+\t\treturn 0;\n+\n+\t/*\n+\t * Some git commands write a commit-graph in-process at the end,\n+\t * prevent their revision walk to interfere with ours.\n+\t */\n+\tclear_object_flags(SEEN | ADDED | SHOWN | TOPO_WALK_EXPLORED |\n+\t\t\t   TOPO_WALK_INDEGREE | UNINTERESTING);\n+\n+\tinit_revisions(&revs, NULL);\n+\tsetup_revisions(ARRAY_SIZE(revs_argv) - 1, revs_argv, &revs, NULL);\n+\tif (prepare_revision_walk(&revs)) {\n+\t\terror(_(\"revision walk setup failed\"));\n+\t\tresult = -1;\n+\t\tgoto cleanup;\n+\t}\n+\n+\tif (ctx->report_progress)\n+\t\tctx->progress = start_delayed_progress(\n+\t\t\t\t_(\"Gathering commit info\"),\n+\t\t\t\t0);\n+\twhile ((commit = get_revision(&revs))) {\n+\t\tdisplay_progress(ctx->progress, ctx->oids.nr + 1);\n+\t\tALLOC_GROW(ctx->oids.list, ctx->oids.nr + 1, ctx->oids.alloc);\n+\t\toidcpy(&ctx->oids.list[ctx->oids.nr], &commit->object.oid);\n+\t\tctx->oids.nr++;\n+\n+\t\tif (!commit->parents) {\n+\t\t\tcreate_modified_path_bloom_filter(&ctx->mpbfctx,\n+\t\t\t\t\t\t\t  commit, 0);\n+\t\t} else {\n+\t\t\tstruct commit_list *parent;\n+\t\t\tint nth_parent;\n+\n+\t\t\tfor (parent = commit->parents, nth_parent = 0;\n+\t\t\t     parent;\n+\t\t\t     parent = parent->next, nth_parent++)\n+\t\t\t\tcreate_modified_path_bloom_filter(&ctx->mpbfctx,\n+\t\t\t\t\t\t\t\t  commit, nth_parent);\n+\t\t}\n+\t}\n+\tstop_progress(&ctx->progress);\n+\n+\tif (ctx->oids.nr >= GRAPH_EDGE_LAST_MASK) {\n+\t\terror(_(\"the commit graph format cannot write %d commits\"), ctx->oids.nr);\n+\t\tresult = -1;\n+\t\tgoto cleanup;\n+\t}\n \n-\tfor_each_ref(add_ref_to_list, &list);\n-\tresult = write_commit_graph(obj_dir, NULL, &list,\n-\t\t\t\t    flags, split_opts);\n+\tQSORT(ctx->oids.list, ctx->oids.nr, oid_compare);\n+\n+\t/*\n+\t * TODO: the revision walking loop above could fill ctx->commits\n+\t * straight away, counting extra edges as well.\n+\t */\n+\tctx->commits.alloc = ctx->oids.nr;\n+\tALLOC_ARRAY(ctx->commits.list, ctx->commits.alloc);\n+\n+\t/*\n+\t * Despite its unassuming name, this function removes duplicate\n+\t * commits/oids and, worse, it does counts extra edges as well.\n+\t */\n+\tcopy_oids_to_commits(ctx);\n+\n+\tif (!ctx->commits.nr)\n+\t\tgoto cleanup;\n+\n+\tif (ctx->split) {\n+\t\tsplit_graph_merge_strategy(ctx);\n \n-\tstring_list_clear(&list, 0);\n+\t\tmerge_commit_graphs(ctx);\n+\t} else\n+\t\tctx->num_commit_graphs_after = 1;\n+\n+\tcompute_generation_numbers(ctx);\n+\n+\tresult = write_commit_graph_file(ctx);\n+\n+\tif (ctx->split)\n+\t\tmark_commit_graphs(ctx);\n+\n+\texpire_commit_graphs(ctx);\n+\n+cleanup:\n+\tfree_write_commit_graph_context(ctx);\n \treturn result;\n }\n \n-- \n2.27.0.rc1.431.g5c813f95dc\n\n"},{"id":"398889","messageId":"82928690-13e5-2881-30d0-d52a93e087d3@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"Re: [PoC PATCH 00/34] An alternative modified path Bloom filters implementation","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2020-05-29T13:59:03Z","receivedAt":"2020-05-29T13:59:08Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/29/2020 4:50 AM, SZEDER Gábor wrote:\n> Sigh...  but better late than never, right?\n\nTrue. The best time to present these ideas would be while the series\nwas under review, which was a reasonable amount of time. But there's\nno helping that, so what can we learn from your extensive work here?\n\n> I experimented quite a bit with modified path Bloom filters a year and\n> more ago, and got quite far...  but my disappointment in the\n> inadequacy of all double hashing schemes, the arrival of split\n> commit-graphs, and, well, life in general has put the whole thing on\n> the back burner, and I haven't touched it for a couple of releases.\n> \n> Now I finally managed to take a closer look at the current changed\n> paths Bloom filters implementation, and saw that it has some of the\n> same issues that I had stumbled upon and that it missed some\n> optimization opportunities.  Unfortunately, fixing those issues and\n> performing those optimizations do require a thorough format change.\n\nMy initial reaction is \"it's too late for a format change now\" but\nthat's not quite true. Even if we ship v2.27.0 with the existing\nformat, the data is in optional chunks. It would not be too difficult\nto replace the storage with a different set of optional chunks and\nstop reading the old ones. The question remains: exactly _how much\nbetter_ is your implementation, and is it enough to justify the\nupheaval?\n\nThe other thing that impacts this consideration is how complicated\nthe format becomes. Bloom filters are already complicated, so how\ncan we keep things simple when possible?\n\n> So here is my proof of concept version, in all its incompleteness,\n> with the following benefits:\n\nThese are all very interesting items. Here is my initial categorization:\n\nThese are bold claims that I hope you have end-to-end performance\nnumbers to back them up. Depending on how much the benefit is, then\nwe can consider a replacing.\n\n>   - Better hashing scheme (though it should be better still).\n>   - Orders of magnitude lower average false positive rate.\n>   - Consequently, faster pathspec-limited revision walks.\n\nThese bullets seem like they may apply to the existing implementation,\nsince they seem unrelated to the format itself. Is it possible to\ncontribute these ideas to the existing implementation first?\n\n>   - Faster processing of the tree-diff output and lower memory usage\n>     while computing Bloom filters (from scratch...).\n>   - Supports multiple pathspecs right from the start.\n>   - Supports some wildcards in pathspecs.\n>   - More cleanups, more bugfixes.\n\nThese items are confusing to me. I either don't know what they mean,\nor doubt the value of having them present.\n\n>   - Better understanding of the problem it tries to optimize.\n>   - Better understanding of the issues with many small Bloom filters.\n>   - Deduplicates Bloom filters.\n>   - It has the right name :)  The diff machinery and all its frontends\n>     report \"modified\" paths with the letter 'M', not \"changed\".\n>   - Handles as many commits as the commit-graph format can>   - Optional support for storing Bloom filters for all parents of\n>     merge commits.\n\n(This last item in particular is one where I have already communicated\nwhy there is little value in storing filters for diffs against a\nsecond parent. Perhaps you have some concrete numbers on what happens\nin terms of storage-vs-performance when adding these diffs.)\n\n> Alas, the drawbacks are significant:\n> \n>   - No tests whatsoever.\n>   - Computes all modified path Bloom filters from scratch when\n>     writing, no matter what.\n>   - Doesn't work with split commit-graphs.\n>   - Basically if anything works besides 'git commit-graph write\n>     --reachable' it's a miracle.\n>   - Not a single test.\n>   - Many BUG()s, which should rather be graceful errors...  though I\n>     have to admit that at this point they are indeed bugs.\n>   - Many TODOs, both in commit messages and code, some incomplete\n>     commit messages, crappy subject lines, even missing signoffs.\n>   - Some ridiculously long variable, function, macro and config\n>     variable names.\n>   - It's based on v2.25.0 (no technical reason, but that's the version\n>     I used to run the baseline benchmarks the last time, which takes\n>     days...)\n>   - I'm pretty sure that there are more...\n>   - Oh, did I mention that there are no tests?\n\nAnd now you get to the _real_ hard part of this feature. It is easy to\ncreate a prototype that implements the idea, but it's another step to\nmake it a full contribution. You clearly know this, which I expect is\nthe reason this was not submitted earlier.\n\n> \n> The first 14 patches are preparatory fixes and cleanups:\n> \n>   01/34 tree-walk.c: don't match submodule entries for 'submod/anything'\n> \n> This fix or something similar is necessary to have consistent output\n> with and without modified path Bloom filters for paths crossing\n> submodule boundary.\n> \n>   02/34 commit-graph: fix parsing the Chunk Lookup table\n> \n> The minimal (though not the best) fix for a bug which, I think, is as\n> old as the commit-graph.  I don't know how to test this.\n> \n>   03/34 commit-graph-format.txt: all multi-byte numbers are in network byte order\n>   04/34 commit-slab: add a function to deep free entries on the slab\n>   05/34 diff.h: drop diff_tree_oid() & friends' return value\n>   06/34 commit-graph: clean up #includes\n> \n> A couple of minor cleanups.\n> \n>   07/34 commit-graph: simplify parse_commit_graph() #1\n>   08/34 commit-graph: simplify parse_commit_graph() #2\n> \n> These two would be the right, though not minimal fix for the parsing\n> bug above.\n> \n>   09/34 commit-graph: simplify write_commit_graph_file() #1\n>   10/34 commit-graph: simplify write_commit_graph_file() #2\n>   11/34 commit-graph: allocate the 'struct chunk_info' array dinamically\n> \n> I think these three cleanup patches are a better alternative of\n> 3be7efcafc (commit-graph: define and use MAX_NUM_CHUNKS, 2020-03-30),\n> because...\n> \n>   12/34 commit-graph: unify the signatures of all write_graph_chunk_*() functions\n>   13/34 commit-graph: simplify write_commit_graph_file() #3\n>   14/34 commit-graph: check chunk sizes after writing\n\nThese 14 patches are probably worth submitting on their own. I hope they\nstill apply cleanly to a recent master.\n\n> ... they laid the ground work for this patch.\n> \n>   15/34 commit-graph-format.txt: document the modified path Bloom filter chunks\n> \n> This is the most important one, specifying and _justifying_ the new\n> chunk formats.\n> \n> Do grab a cup or pint of your favourite beverage and get comfy before\n> reading this one.  You have been warned.\n> \n>   16/34 Add a generic and minimal Bloom filter implementation\n>   17/34 Import a streaming-capable Murmur3 hash function implementation\n>   18/34 commit-graph: write \"empty\" Modified Path Bloom Filter Index chunk\n>   19/34 commit-graph: add commit slab for modified path Bloom filters\n>   20/34 commit-graph: fill the Modified Path Bloom Filter Index chunk\n> \n> This shows a more efficient approach to process the tree-diff output\n> into Bloom filters.\n> \n>   21/34 commit-graph: load and use the Modified Path Bloom Filter Index chunk\n>   22/34 commit-graph: write the Modified Path Bloom Filters chunk\n>   23/34 commit-graph: load and use the Modified Path Bloom Filters chunk\n\nOk, these implement the new filter format.\n\n>   24/34 commit-graph: check all leading directories in modified path Bloom filters\n> \n> This was a good lightbulb moment.  It is essential to try to maintain\n> reasonable performance in repositories where the vast majority of\n> changes are concentrated to a single directory.\n\nThis is really interesting! This could apply to the existing format, so\nwhen you compare formats this patch should either be in both or in neither.\n \n>   25/34 commit-graph: check embedded modified path Bloom filters with a mask\n>   26/34 commit-graph: deduplicate modified path Bloom filters\n>   27/34 commit-graph: load modified path Bloom filters for merge commits\n>   28/34 commit-graph: write Modified Path Bloom Filter Merge Index chunk\n>   29/34 commit-graph: extract init and free write_commit_graph_context\n>   30/34 commit-graph: move write_commit_graph_reachable below write_commit_graph\n>   31/34 t7007-show: make the first test compatible with the next patch\n>   32/34 PoC commit-graph: use revision walk machinery for '--reachable'\n> \n> Once upon a time I thought this was the greatest idea ever, but as\n> time goes by I get more and more concerned that this is a really dumb\n> idea, though don't yet know why.\n\nOk, here are some extra patches that implement your extra features. The\nmulti-parent filters are a fair thing to compare against the existing\nimplementation, but we need to compare file size changes, extra computation\ntime changes, and how much the end-to-end rev-list performance changes.\nAre the costs justified?\n\n>   33/34 commit-graph: write modified path Bloom filters in \"history order\"\n\nI think we've covered this in these two patches:\n\n3d112755056 commit-graph: examine commits by generation number\nd21ee7d1110 commit-graph: examine changed-path objects in pack order\n\n>   34/34 commit-graph: use modified path Bloom filters with wildcards, if possible\n> \n> Finally a cherry on top.\n\nThis is also something that could be interesting for the current\nimplementation. Perhaps group it with patches 1-14 and 24, if the idea\napplies to the existing implementation.\n\nI think it would be great to apply whatever code cleanups and fixes\nthat you've presented here to the existing implementation _first_,\nthen we have the optimal way to compare how your proposed format\nimproves over the existing implementation.\n \nI can't commit more time to this series today, but hopefully the discussion\nis just getting started. I'll try to poke around some of the patches\nnext week.\n\nThanks,\n-Stolee\n"},{"id":"398891","messageId":"nycvar.QRO.7.76.6.2005282359060.56@tvgsbejvaqbjf.bet","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"Re: [PoC PATCH 00/34] An alternative modified path Bloom filters implementation","fromName":"Johannes Schindelin","fromEmail":"johannes.schindelin@gmx.de","sentAt":"2020-05-28T22:09:44Z","receivedAt":"2020-05-29T14:43:45Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Hi Gábor,\n\nOn Fri, 29 May 2020, SZEDER Gábor wrote:\n\n> Sigh...  but better late than never, right?\n\nWell, incremental patches on top of what is _already_ in Git are _even_\nbetter.\n\n> I experimented quite a bit with modified path Bloom filters a year and\n> more ago, and got quite far...  but my disappointment in the\n> inadequacy of all double hashing schemes, the arrival of split\n> commit-graphs, and, well, life in general has put the whole thing on\n> the back burner, and I haven't touched it for a couple of releases.\n>\n> Now I finally managed to take a closer look at the current changed\n> paths Bloom filters implementation, and saw that it has some of the\n> same issues that I had stumbled upon and that it missed some\n> optimization opportunities.  Unfortunately, fixing those issues and\n> performing those optimizations do require a thorough format change.\n\nThank you for this background information.\n\nYour claim that there are opportunities to optimize, as well as your claim\nthat it requires a thorough format change, strike me as best backed up\nwith evidence by porting your optimizations on top of what is already in\n`master` and which has been reviewed _already_ (as opposed to your new\npatch series, which essentially threatens to throw away all the time\npeople spent on reviewing Garima's patches).\n\n> So here is my proof of concept version, in all its incompleteness,\n> with the following benefits:\n>\n>   - Better understanding of the problem it tries to optimize.\n>   - Better understanding of the issues with many small Bloom filters.\n>   - Better hashing scheme (though it should be better still).\n>   - Orders of magnitude lower average false positive rate.\n>   - Consequently, faster pathspec-limited revision walks.\n>   - Faster processing of the tree-diff output and lower memory usage\n>     while computing Bloom filters (from scratch...).\n>   - Optional support for storing Bloom filters for all parents of\n>     merge commits.\n>   - Deduplicates Bloom filters.\n>   - Supports multiple pathspecs right from the start.\n>   - Supports some wildcards in pathspecs.\n>   - Handles as many commits as the commit-graph format can.\n>   - It has the right name :)  The diff machinery and all its frontends\n>     report \"modified\" paths with the letter 'M', not \"changed\".\n>   - More cleanups, more bugfixes.\n>   - Consistent output with and without modified path Bloom filters for\n>     over 80k random paths in 16 repositories, even with submodules in\n>     them.  Well, at least on my machine, if nowhere else...\n\nIt strikes me as an obvious idea to make all those improvements in an\nincremental manner.\n\n> Alas, the drawbacks are significant:\n>\n>   - No tests whatsoever.\n>   - Computes all modified path Bloom filters from scratch when\n>     writing, no matter what.\n>   - Doesn't work with split commit-graphs.\n>   - Basically if anything works besides 'git commit-graph write\n>     --reachable' it's a miracle.\n>   - Not a single test.\n\nYou said that already in the first bullet point. But yes, I get it, there\nare no tests.\n\n>   - Many BUG()s, which should rather be graceful errors...  though I\n>     have to admit that at this point they are indeed bugs.\n>   - Many TODOs, both in commit messages and code, some incomplete\n>     commit messages, crappy subject lines, even missing signoffs.\n>   - Some ridiculously long variable, function, macro and config\n>     variable names.\n>   - It's based on v2.25.0 (no technical reason, but that's the version\n>     I used to run the baseline benchmarks the last time, which takes\n>     days...)\n>   - I'm pretty sure that there are more...\n>   - Oh, did I mention that there are no tests?\n\nAh, your patch series has no tests.\n\nPlease do understand that it can be perceived as quite frustrating that\nan alternative is presented _this_ late, when already such a lot of effort\nhas gone into not only iterating several times on the patch series but\nalso into reviewing all of them.\n\nI don't really think that I want to spend the time to review this\nalternative (not that I am an expert in the space) because it would imply\nthat I help invalidating the effort that went into the current\nimplementation.\n\nAll this is to say: I appreciate your efforts to improve the path-based\nBloom filters, at the same time I wish that those efforts would happen in\na collaborative manner instead of simply dismissing other people's work.\n\nCiao,\nJohannes\n"},{"id":"399006","messageId":"20200601232504.GA42750@syl.local","threadId":"53579","inReplyTo":"20200529085038.26008-1-szeder.dev@gmail.com","subject":"Re: [PoC PATCH 00/34] An alternative modified path Bloom filters implementation","fromName":"Taylor Blau","fromEmail":"me@ttaylorr.com","sentAt":"2020-06-01T23:25:04Z","receivedAt":"2020-06-01T23:25:11Z","isPatch":true,"sender":{"key":"me@ttaylorr.com","avatar":"https://avatars.githubusercontent.com/u/301000140?v=4"},"body":"On Fri, May 29, 2020 at 10:50:04AM +0200, SZEDER Gábor wrote:\n> Sigh...  but better late than never, right?\n\nYes, indeed. I think that there is a balance here: I'm thrilled that you\nare choosing to spend your time working on and improving the\nchanged-path Bloom filter implementation.\n\nOf course, it couldn't have hurt to have these ideas earlier when the\nlist was more focused on reviewing Garima's original patches. But,\nnothing is set in stone, and it seems like there are some re-usable\nideas and clean-ups below.\n\nI'll respond to the patch descriptions below with a \"cut-line\" where I\nthink we could extract and apply what's already there before putting the\nrest aside to be worked on more.\n\n> I experimented quite a bit with modified path Bloom filters a year and\n> more ago, and got quite far...  but my disappointment in the\n> inadequacy of all double hashing schemes, the arrival of split\n> commit-graphs, and, well, life in general has put the whole thing on\n> the back burner, and I haven't touched it for a couple of releases.\n>\n> Now I finally managed to take a closer look at the current changed\n> paths Bloom filters implementation, and saw that it has some of the\n> same issues that I had stumbled upon and that it missed some\n> optimization opportunities.  Unfortunately, fixing those issues and\n> performing those optimizations do require a thorough format change.\n>\n> So here is my proof of concept version, in all its incompleteness,\n> with the following benefits:\n>\n>   - Better understanding of the problem it tries to optimize.\n>   - Better understanding of the issues with many small Bloom filters.\n>   - Better hashing scheme (though it should be better still).\n>   - Orders of magnitude lower average false positive rate.\n>   - Consequently, faster pathspec-limited revision walks.\n>   - Faster processing of the tree-diff output and lower memory usage\n>     while computing Bloom filters (from scratch...).\n>   - Optional support for storing Bloom filters for all parents of\n>     merge commits.\n>   - Deduplicates Bloom filters.\n>   - Supports multiple pathspecs right from the start.\n>   - Supports some wildcards in pathspecs.\n>   - Handles as many commits as the commit-graph format can.\n>   - It has the right name :)  The diff machinery and all its frontends\n>     report \"modified\" paths with the letter 'M', not \"changed\".\n>   - More cleanups, more bugfixes.\n>   - Consistent output with and without modified path Bloom filters for\n>     over 80k random paths in 16 repositories, even with submodules in\n>     them.  Well, at least on my machine, if nowhere else...\n>\n> Alas, the drawbacks are significant:\n>\n>   - No tests whatsoever.\n\nYes, obviously this needs to be addressed, but I certainly don't fault\nyou for posting what you have (thanks for adding the 'PoC' prefix to\nindicate it as such).\n\n>   - Computes all modified path Bloom filters from scratch when\n>     writing, no matter what.\n>   - Doesn't work with split commit-graphs.\n\nI originally thought that this would be a non-starter, but it may not\nbe. GitHub doesn't write Bloom filters at all (yet), but one\nconsideration of ours is to only generate Bloom filters when we roll-up\nall of the split graphs.\n\nOur invocation during this bigger repository maintenance is something\nlike:\n\n  $ git commit-graph write --reachable --split=replace\n\nBut if it turns out to be expensive to run 'git commit-graph write\n--split=no-merge --stdin-commits --write --changed-paths' on every push,\nwe might just run the above with '--changed-paths'.\n\nI can imagine other schemes where it would be desirable to have your\nBloom filter implementation over split graphs, in which case this should\nbe addressed. But, I don't have a problem with a version that only works\nfor non-split graphs first, so long as there isn't a compatibility\nboundary between that and a version that does work for split graphs.\n\n>   - Basically if anything works besides 'git commit-graph write\n>     --reachable' it's a miracle.\n>   - Not a single test.\n>   - Many BUG()s, which should rather be graceful errors...  though I\n>     have to admit that at this point they are indeed bugs.\n>   - Many TODOs, both in commit messages and code, some incomplete\n>     commit messages, crappy subject lines, even missing signoffs.\n>   - Some ridiculously long variable, function, macro and config\n>     variable names.\n>   - It's based on v2.25.0 (no technical reason, but that's the version\n>     I used to run the baseline benchmarks the last time, which takes\n>     days...)\n>   - I'm pretty sure that there are more...\n>   - Oh, did I mention that there are no tests?\n\n...I think so ;-).\n\n>\n> The first 14 patches are preparatory fixes and cleanups:\n>\n>   01/34 tree-walk.c: don't match submodule entries for 'submod/anything'\n>\n> This fix or something similar is necessary to have consistent output\n> with and without modified path Bloom filters for paths crossing\n> submodule boundary.\n>\n>   02/34 commit-graph: fix parsing the Chunk Lookup table\n>\n> The minimal (though not the best) fix for a bug which, I think, is as\n> old as the commit-graph.  I don't know how to test this.\n>\n>   03/34 commit-graph-format.txt: all multi-byte numbers are in network byte order\n>   04/34 commit-slab: add a function to deep free entries on the slab\n>   05/34 diff.h: drop diff_tree_oid() & friends' return value\n>   06/34 commit-graph: clean up #includes\n>\n> A couple of minor cleanups.\n>\n>   07/34 commit-graph: simplify parse_commit_graph() #1\n>   08/34 commit-graph: simplify parse_commit_graph() #2\n>\n> These two would be the right, though not minimal fix for the parsing\n> bug above.\n>\n>   09/34 commit-graph: simplify write_commit_graph_file() #1\n>   10/34 commit-graph: simplify write_commit_graph_file() #2\n>   11/34 commit-graph: allocate the 'struct chunk_info' array dinamically\n>\n> I think these three cleanup patches are a better alternative of\n> 3be7efcafc (commit-graph: define and use MAX_NUM_CHUNKS, 2020-03-30),\n> because...\n>\n>   12/34 commit-graph: unify the signatures of all write_graph_chunk_*() functions\n>   13/34 commit-graph: simplify write_commit_graph_file() #3\n>   14/34 commit-graph: check chunk sizes after writing\n\nI think you're right to draw the \"laying the ground work\" line here.\nCould these first fourteen patches be applied cleanly to master? They\nall look mostly like improvements to me, especially the second patch.\n\n> ... they laid the ground work for this patch.\n>\n>   15/34 commit-graph-format.txt: document the modified path Bloom filter chunks\n>\n> This is the most important one, specifying and _justifying_ the new\n> chunk formats.\n>\n> Do grab a cup or pint of your favourite beverage and get comfy before\n> reading this one.  You have been warned.\n>\n>   16/34 Add a generic and minimal Bloom filter implementation\n>   17/34 Import a streaming-capable Murmur3 hash function implementation\n>   18/34 commit-graph: write \"empty\" Modified Path Bloom Filter Index chunk\n>   19/34 commit-graph: add commit slab for modified path Bloom filters\n>   20/34 commit-graph: fill the Modified Path Bloom Filter Index chunk\n>\n> This shows a more efficient approach to process the tree-diff output\n> into Bloom filters.\n>\n>   21/34 commit-graph: load and use the Modified Path Bloom Filter Index chunk\n>   22/34 commit-graph: write the Modified Path Bloom Filters chunk\n>   23/34 commit-graph: load and use the Modified Path Bloom Filters chunk\n>   24/34 commit-graph: check all leading directories in modified path Bloom filters\n>\n> This was a good lightbulb moment.  It is essential to try to maintain\n> reasonable performance in repositories where the vast majority of\n> changes are concentrated to a single directory.\n>\n>   25/34 commit-graph: check embedded modified path Bloom filters with a mask\n>   26/34 commit-graph: deduplicate modified path Bloom filters\n>   27/34 commit-graph: load modified path Bloom filters for merge commits\n>   28/34 commit-graph: write Modified Path Bloom Filter Merge Index chunk\n>   29/34 commit-graph: extract init and free write_commit_graph_context\n>   30/34 commit-graph: move write_commit_graph_reachable below write_commit_graph\n>   31/34 t7007-show: make the first test compatible with the next patch\n>   32/34 PoC commit-graph: use revision walk machinery for '--reachable'\n>\n> Once upon a time I thought this was the greatest idea ever, but as\n> time goes by I get more and more concerned that this is a really dumb\n> idea, though don't yet know why.\n>\n>   33/34 commit-graph: write modified path Bloom filters in \"history order\"\n>   34/34 commit-graph: use modified path Bloom filters with wildcards, if possible\n>\n> Finally a cherry on top.\n\nThe next twenty patches look like they need some more work. At a\nhigh-level, I think we need to do the following things, in roughly the\nfollowing order:\n\n  - Prove that this is a strict performance improvement on the existing\n    Bloom filter implementation.\n\n  - Sketch out a way that it would work for cases where '--split' is\n    provided, and/or '--reachable' isn't provided.\n\n  - Clean up the existing patches, adding tests which exercise the new\n    functionality, and prove that there aren't regressions against the\n    existing Bloom filter implementation.\n\n  - Review and repeat.\n\nI guess I'm left wondering whether or not this is something that you\nwant to continue, given that I outlined a pretty lengthy path for\ngetting this in a suitable shape to keep going.\n\nAre you planning on keeping working on this?\n\nThanks,\nTaylor\n"},{"id":"399036","messageId":"xmqqeeqxtmy2.fsf@gitster.c.googlers.com","threadId":"53579","inReplyTo":"20200601232504.GA42750@syl.local","subject":"Re: [PoC PATCH 00/34] An alternative modified path Bloom filters implementation","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2020-06-02T17:08:37Z","receivedAt":"2020-06-02T17:08:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Taylor Blau <me@ttaylorr.com> writes:\n\n> On Fri, May 29, 2020 at 10:50:04AM +0200, SZEDER Gábor wrote:\n>> Sigh...  but better late than never, right?\n>\n> Yes, indeed. I think that there is a balance here: I'm thrilled that you\n> are choosing to spend your time working on and improving the\n> changed-path Bloom filter implementation.\n>\n> Of course, it couldn't have hurt to have these ideas earlier when the\n> list was more focused on reviewing Garima's original patches. But,\n> nothing is set in stone, and it seems like there are some re-usable\n> ideas and clean-ups below.\n\nYes, I had the same impression as you did, unlike some folks who\nsounded as if they felt offended seeing a comment that sabotages\nexisting work.  It could have been presented in a more useful ways\n(i.e. instead of risking to appear suggesting total replacement,\nwhich I do not think was the intention, massaged to build on top of\nwhat is already there as improvements), though.\n\n> I think you're right to draw the \"laying the ground work\" line here.\n> Could these first fourteen patches be applied cleanly to master? They\n> all look mostly like improvements to me, especially the second patch.\n\nAgain, I concur.\n\nThanks.\n"},{"id":"399040","messageId":"20200602175949.GA2898@szeder.dev","threadId":"53579","inReplyTo":"20200529085038.26008-16-szeder.dev@gmail.com","subject":"Re: [PATCH 15/34] commit-graph-format.txt: document the modified path Bloom filter chunks","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-06-02T17:59:49Z","receivedAt":"2020-06-02T18:00:02Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, May 29, 2020 at 10:50:19AM +0200, SZEDER Gábor wrote:\n> Modified Path Bloom Filters\n\n> Each modified path Bloom filter consists of:\n> \n>   - 4 bytes specifying the number of bits in the Bloom filter's bit\n>     array.\n> \n>     For practical purposes 32 bit is more than sufficient to store the\n>     number of bits in the Bloom filter's array.  When using k = 7\n>     hashes, i.e. 10 bits per path, then we could store over 400\n>     million paths in a single Bloom filter; with k = 32 hashes we'd\n>     use 46 bit per path, which is still over 93 million paths.\n\n> Alternatives considered\n> -----------------------\n> \n> Here are some alternatives that I've considered but discarded and\n> ideas that I haven't (yet) followed through:\n\n>   - Varints.  Using 4 bytes for the size of each Bloom filter in the\n>     Modified Path Bloom Filters chunk is a lot more than necessary.\n>     How much space can be saved by using varints?\n\nNot that much, at least compared to the whole commit-graph file.\n\nSince 63 bit modified path Bloom filters are embedded in the index\nentries, it's pointless to store smaller Bloom filters, so the size of\nnon-embedded Bloom filters can be defined as\n\n  nr_bits = 64 + decode_varint(...)\n\nA one byte varint can encode values up to 127, while a two bytes\nvarint can encode values up to 16511.  So the max nr_bits is 191 and\n16575, respectively, which means max 19 or 1657 paths with 7 hashes,\ni.e. 10 bits per path.  The table below shows the percentage of\ncommits with embedded modified path Bloom filters and commits with\nnon-embedded Bloom filters with one byte and two bytes varints, and\nhow much space is saved.\n\n                    Percentage of commits   |\n                    modifying <= N paths    |   varint   commit-graph\n                  compared to first parent  | size diff    file size\n                  <=6      <=19     <=1657  |  (bytes)     (ls -lh)\n  ------------------------------------------+-------------------------------\n  elasticsearch  18.32%   65.56%    99.79%  |   -90158       9.5M      -0.9%\n  jdk            26.62%   70.46%    97.94%  |   -90262        16M      -0.6%\n  webkit         38.42%   82.24%    99.92%  |  -298824        19M      -1.6%\n  android-base   42.32%   88.02%    99.98%  |  -132327        30M      -0.5%\n  llvm-project   53.60%   93.86%    99.99%  |  -344239        24M      -1.4%\n  gecko-dev      54.54%   87.15%    99.87%  |  -625029        63M      -1.0%\n  tensorflow     55.17%   90.76%    99.52%  |   -83758       9.0M      -0.9%\n  rails          58.71%   95.13%    99.99%  |   -53153       6.0M      -0.9%\n  rust           59.29%   88.03%    99.96%  |   -90677       8.2M      -1.1%\n  glibc          61.59%   91.11%    99.96%  |   -38422       3.8M      -1.0%\n  gcc            63.80%   95.39%    99.97%  |  -179463        18M      -1.0%\n  go             65.31%   95.19%    99.99%  |   -39109       3.2M      -1.2%\n  cmssw          67.58%   93.02%    99.91%  |  -154440        23M      -0.7%\n  linux          72.79%   95.27%    99.78%  |  -527045        78M      -0.7%\n  cpython        81.91%   97.78%   100.00%  |   -40678       7.8M      -0.5%\n  git            90.28%   98.56%   100.00%  |   -13406       3.9M      -0.4%\n  homebrew-cask  98.61%   99.58%    99.99%  |    -2992       6.6M      -0.1%\n  homebrew-core  99.81%   99.93%   100.00%  |     -810        11M      -0.1%\n\n>     How much is the runtime overhead of decoding those varints?\n\nNot enough to be above noise level.  (\"a lot of paths\",\nMurmurHash3, k = 7, enhanced double hashing, 32 bit uint arithmetic)\n\n                                       |   Average time spent\n                   Average runtime     |  loading and querying\n                                       |      Bloom filters\n                  uint32     varint    |   uint32     varint\n  -------------------------------------+-----------------------------\n  android-base    0.1456s   0.144172s  |  0.01550s   0.01571s    1.3%\n  cmssw           0.0313s   0.032046s  |  0.00383s   0.00395s    3.1%\n  cpython         0.0810s   0.081649s  |  0.00765s   0.00785s    2.6%\n  elasticsearch   0.0136s   0.013899s  |  0.00246s   0.00258s    4.8%\n  gcc             0.2114s   0.211080s  |  0.02259s   0.02261s      0%\n  gecko-dev       0.4815s   0.474903s  |  0.06004s   0.06028s    0.3%\n  git             0.0310s   0.031156s  |  0.00192s   0.00195s    1.5%\n  glibc           0.0282s   0.029032s  |  0.00320s   0.00342s    6.8%\n  go              0.0403s   0.039408s  |  0.00428s   0.00414s   -3.3%\n  jdk             0.0068s   0.006666s  |  0.00117s   0.00113s   -3.5%\n  linux           0.0873s   0.087438s  |  0.01109s   0.01133s    2.1%\n  llvm-project    0.4135s   0.418390s  |  0.04716s   0.04834s    2.5%\n  rails           0.0391s   0.038997s  |  0.00449s   0.00448s   -0.3%\n  rust            0.0439s   0.044569s  |  0.00444s   0.00461s    3.8%\n  tensorflow      0.0487s   0.049166s  |  0.00618s   0.00634s    2.5%\n  webkit          0.2420s   0.241807s  |  0.03353s   0.03379s    0.7%\n\n>     How can we ensure that varint decoding doesn't read past the end\n>     of the mmapped memory region?\n\nWith a decode_varint() variant that takes the max number of bytes to\nread as an additional parameter, and returns 0 if the varint is too\nlong.\n\n"},{"id":"399123","messageId":"8030939b-44fe-e251-5b76-2b2f260b980f@gmail.com","threadId":"53579","inReplyTo":"20200529085038.26008-5-szeder.dev@gmail.com","subject":"Re: [PATCH 04/34] commit-slab: add a function to deep free entries on the slab","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2020-06-04T16:43:32Z","receivedAt":"2020-06-04T16:43:37Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 5/29/2020 4:50 AM, SZEDER Gábor wrote:\n> +void free_depth_in_slab(int **ptr)\n\nThis needs to be \"static\" to compile with DEVELOPER=1.\n\n(I'm working to apply your patches on top of ds/line-log-on-bloom\nso we can get the benefit of these universally-good things. I just\nwant to report the things that I stumble on.)\n\nThanks,\n-Stolee\n\n"},{"id":"400730","messageId":"20200627155610.GN2898@szeder.dev","threadId":"53579","inReplyTo":"8030939b-44fe-e251-5b76-2b2f260b980f@gmail.com","subject":"Re: [PATCH 04/34] commit-slab: add a function to deep free entries on the slab","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2020-06-27T15:56:10Z","receivedAt":"2020-06-27T15:56:16Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Thu, Jun 04, 2020 at 12:43:32PM -0400, Derrick Stolee wrote:\n> On 5/29/2020 4:50 AM, SZEDER Gábor wrote:\n> > +void free_depth_in_slab(int **ptr)\n> \n> This needs to be \"static\" to compile with DEVELOPER=1.\n\nYou are right, of course, but...\n\nDo you get an error because of this when building with DEVELOPER=1?  I\ndon't get an error when building with DEVELOPER=1...\n\n> (I'm working to apply your patches on top of ds/line-log-on-bloom\n> so we can get the benefit of these universally-good things. I just\n> want to report the things that I stumble on.)\n\nThanks.\n\n"},{"id":"400766","messageId":"b2fa923f-adf2-9cc8-7f4a-80a08aaf346f@gmail.com","threadId":"53579","inReplyTo":"20200627155610.GN2898@szeder.dev","subject":"Re: [PATCH 04/34] commit-slab: add a function to deep free entries on the slab","fromName":"Derrick Stolee","fromEmail":"stolee@gmail.com","sentAt":"2020-06-29T11:30:12Z","receivedAt":"2020-06-29T20:55:32Z","isPatch":true,"sender":{"key":"stolee@gmail.com","avatar":"https://avatars.githubusercontent.com/u/570044?v=4"},"body":"On 6/27/2020 11:56 AM, SZEDER Gábor wrote:\n> On Thu, Jun 04, 2020 at 12:43:32PM -0400, Derrick Stolee wrote:\n>> On 5/29/2020 4:50 AM, SZEDER Gábor wrote:\n>>> +void free_depth_in_slab(int **ptr)\n>>\n>> This needs to be \"static\" to compile with DEVELOPER=1.\n> \n> You are right, of course, but...\n> \n> Do you get an error because of this when building with DEVELOPER=1?  I\n> don't get an error when building with DEVELOPER=1...\n\nYes, I believe that's how I discovered it.\n\n>> (I'm working to apply your patches on top of ds/line-log-on-bloom\n>> so we can get the benefit of these universally-good things. I just\n>> want to report the things that I stumble on.)\n> \n> Thanks.\n\nSure thing.\n-Stolee\n\n"}]}