merge-ort: implement support for packing objects together
cover merge-ort: implement support for packing objects together
(Based on the tip of 'eb/limit-bulk-checkin-to-blobs'.)
This series implements support for a new merge-tree option, --write-pack, which causes any newly-written objects to be packed together instead of being stored individually as loose.
The motivating use-case behind these changes is to better support repositories who invoke merge-tree frequently, generating a potentially large number of loose objects, resulting in a possible adverse effect on performance.
The majority of the changes here are preparatory to refactor common routines out of the bulk-checkin machinery to prepare for indexing different types of objects whose contents can be held in-core.
Also worth noting is the relative ease this series can be adapted to support the $NEW_HASH interop work in 'eb/hash-transition-rfc'. For more details on the relatively small number of changes necessary to make that work, see the log message of the second-to-last patch.
Thanks in advance for your review!
Taylor Blau (7):
bulk-checkin: factor out format_object_header_hash()
bulk-checkin: factor out prepare_checkpoint()
bulk-checkin: factor out truncate_checkpoint()
bulk-checkin: factor our finalize_checkpoint()
bulk-checkin: introduce index_blob_bulk_checkin_incore()
bulk-checkin: introduce index_tree_bulk_checkin_incore() builtin/merge-tree.c: implement support for --write-pack
Documentation/git-merge-tree.txt | 4 + builtin/merge-tree.c | 5 + bulk-checkin.c | 249 ++++++++++++++++++++++++++----- bulk-checkin.h | 8 + merge-ort.c | 43 ++++-- merge-recursive.h | 1 + t/t4301-merge-tree-write-tree.sh | 93 ++++++++++++ 7 files changed, 355 insertions(+), 48 deletions(-)
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
PATCH 1/7 bulk-checkin: factor out `format_object_header_hash()`
Before deflating a blob into a pack, the bulk-checkin mechanism prepares the pack object header by calling format_object_header(), and writing into a scratch buffer, the contents of which eventually makes its way into the pack.
Future commits will add support for deflating multiple kinds of objects into a pack, and will likewise need to perform a similar operation as below.
This is a mostly straightforward extraction, with one notable exception. Instead of hard-coding the_hash_algo, pass it in to the new function as an argument. This isn't strictly necessary for our immediate purposes here, but will prove useful in the future if/when the bulk-checkin mechanism grows support for the hash transition plan.
--- bulk-checkin.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-)
bulk-checkin.c +14 -6
@@ -247,6 +247,19 @@ static void prepare_to_stream(struct bulk_checkin_packfile *state,die_errno ("unable to write pack header" ); } +static void format_object_header_hash (const struct git_hash_algo *algop, +git_hash_ctx *ctx,enum object_type type, +size_t size) +{ +unsigned char header[16384 ]; +unsigned header_len =format_object_header ((char *)header, +sizeof (header), + type, size); + + algop->init_fn (ctx); + algop->update_fn (ctx, header, header_len); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -254,8 +267,6 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, {off_t seekback, already_hashed_to;git_hash_ctx ctx; -unsigned char obuf[16384 ]; -unsigned header_len;struct hashfile_checkpoint checkpoint = {0 };struct pack_idx_entry *idx =NULL ;
@@ -263,10 +274,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (seekback == (off_t) -1 )return error ("cannot find the current offset" ); - header_len =format_object_header ((char *)obuf,sizeof (obuf), -OBJ_BLOB , size); - the_hash_algo->init_fn (&ctx); - the_hash_algo->update_fn (&ctx, obuf, header_len); +format_object_header_hash (the_hash_algo, &ctx,OBJ_BLOB , size);/* Note: idx is non-NULL when we are writing */ if ((flags &HASH_WRITE_OBJECT ) !=0 )
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
PATCH 2/7 bulk-checkin: factor out `prepare_checkpoint()`
In reply to Taylor BlauIn a similar spirit as the previous commit, factor out the routine to prepare streaming into a bulk-checkin pack into its own function. Unlike the previous patch, this is a verbatim copy and paste.
--- bulk-checkin.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-)
bulk-checkin.c +14 -6
@@ -260,6 +260,19 @@ static void format_object_header_hash(const struct git_hash_algo *algop, algop->update_fn (ctx, header, header_len); } +static void prepare_checkpoint (struct bulk_checkin_packfile *state, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx, +unsigned flags) +{ +prepare_to_stream (state, flags); +if (idx) { +hashfile_checkpoint (state->f, checkpoint); + idx->offset = state->offset; +crc32_begin (state->f); + } +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -283,12 +296,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, already_hashed_to =0 ;while (1 ) { -prepare_to_stream (state, flags); -if (idx) { -hashfile_checkpoint (state->f, &checkpoint); - idx->offset = state->offset; -crc32_begin (state->f); - } +prepare_checkpoint (state, &checkpoint, idx, flags);if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, fd, size, path, flags))break ;
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
PATCH 3/7 bulk-checkin: factor out `truncate_checkpoint()`
In reply to Taylor BlauIn a similar spirit as previous commits, factor our the routine to truncate a bulk-checkin packfile when writing past the pack size limit.
--- bulk-checkin.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-)
bulk-checkin.c +17 -10
@@ -273,6 +273,22 @@ static void prepare_checkpoint(struct bulk_checkin_packfile *state, } } +static void truncate_checkpoint (struct bulk_checkin_packfile *state, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx) +{ +/* +* Writing this object to the current pack will make +* it too big; we need to truncate it, start a new +* pack, and write into it. +*/ +if (!idx) +BUG ("should not happen" ); +hashfile_truncate (state->f, checkpoint); + state->offset = checkpoint->offset; +flush_bulk_checkin_packfile (state); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -300,16 +316,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, fd, size, path, flags))break ; -/* -* Writing this object to the current pack will make -* it too big; we need to truncate it, start a new -* pack, and write into it. -*/ -if (!idx) -BUG ("should not happen" ); -hashfile_truncate (state->f, &checkpoint); - state->offset = checkpoint.offset; -flush_bulk_checkin_packfile (state); +truncate_checkpoint (state, &checkpoint, idx);if (lseek (fd, seekback,SEEK_SET ) == (off_t) -1 )return error ("cannot seek back" ); }
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
PATCH 4/7 bulk-checkin: factor our `finalize_checkpoint()`
In reply to Taylor BlauIn a similar spirit as previous commits, factor out the routine to finalize the just-written object from the bulk-checkin mechanism.
--- bulk-checkin.c | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-)
bulk-checkin.c +25 -16
@@ -289,6 +289,30 @@ static void truncate_checkpoint(struct bulk_checkin_packfile *state,flush_bulk_checkin_packfile (state); } +static void finalize_checkpoint (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx, +struct object_id *result_oid) +{ + the_hash_algo->final_oid_fn (result_oid, ctx); +if (!idx) +return ; + + idx->crc32 =crc32_end (state->f); +if (already_written (state, result_oid)) { +hashfile_truncate (state->f, checkpoint); + state->offset = checkpoint->offset; +free (idx); + }else { +oidcpy (&idx->oid, result_oid); +ALLOC_GROW (state->written, + state->nr_written +1 , + state->alloc_written); + state->written[state->nr_written++] = idx; + } +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -320,22 +344,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (lseek (fd, seekback,SEEK_SET ) == (off_t) -1 )return error ("cannot seek back" ); } - the_hash_algo->final_oid_fn (result_oid, &ctx); -if (!idx) -return 0 ; - - idx->crc32 =crc32_end (state->f); -if (already_written (state, result_oid)) { -hashfile_truncate (state->f, &checkpoint); - state->offset = checkpoint.offset; -free (idx); - }else { -oidcpy (&idx->oid, result_oid); -ALLOC_GROW (state->written, - state->nr_written +1 , - state->alloc_written); - state->written[state->nr_written++] = idx; - } +finalize_checkpoint (state, &ctx, &checkpoint, idx, result_oid);return 0 ; }
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
PATCH 5/7 bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
In reply to Taylor BlauNow that we have factored out many of the common routines necessary to index a new object into a pack created by the bulk-checkin machinery, we can introduce a variant of index_blob_bulk_checkin() that acts on blobs whose contents we can fit in memory.
This will be useful in a couple of more commits in order to provide the merge-tree builtin with a mechanism to create a new pack containing any objects it created during the merge, instead of storing those objects individually as loose.
Similar to the existing index_blob_bulk_checkin() function, the entrypoint delegates to deflate_blob_to_pack_incore(), which is responsible for formatting the pack header and then deflating the contents into the pack. The latter is accomplished by calling deflate_blob_contents_to_pack_incore(), which takes advantage of the earlier refactoring and is responsible for writing the object to the pack and handling any overage from pack.packSizeLimit.
The bulk of the new functionality is implemented in the function stream_obj_to_pack_incore(), which is a generic implementation for writing objects of arbitrary type (whose contents we can fit in-core) into a bulk-checkin pack.
The new function shares an unfortunate degree of similarity to the existing stream_blob_to_pack() function. But DRY-ing up these two would likely be more trouble than it's worth, since the latter has to deal with reading and writing the contents of the object.
Consistent with the rest of the bulk-checkin mechanism, there are no direct tests here. In future commits when we expose this new functionality via the merge-tree builtin, we will test it indirectly there.
--- bulk-checkin.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ bulk-checkin.h | 4 ++ 2 files changed, 120 insertions(+)
bulk-checkin.c +116 -0
@@ -140,6 +140,69 @@ static int already_written(struct bulk_checkin_packfile *state, struct object_idreturn 0 ; } +static int stream_obj_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +off_t *already_hashed_to, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +git_zstream s; +unsigned char obuf[16384 ]; +unsigned hdrlen; +int status =Z_OK ; +int write_object = (flags &HASH_WRITE_OBJECT ); + +git_deflate_init (&s, pack_compression_level); + + hdrlen =encode_in_pack_object_header (obuf,sizeof (obuf), type, size); + s.next_out = obuf + hdrlen; + s.avail_out =sizeof (obuf) - hdrlen; + +if (*already_hashed_to < size) { +size_t hsize = size - *already_hashed_to; +if (hsize) { + the_hash_algo->update_fn (ctx, buf, hsize); + } + *already_hashed_to = size; + } + s.next_in = (void *)buf; + s.avail_in = size; + +while (status !=Z_STREAM_END ) { + status =git_deflate (&s,Z_FINISH ); +if (!s.avail_out || status ==Z_STREAM_END ) { +if (write_object) { +size_t written = s.next_out - obuf; + +/* would we bust the size limit? */ +if (state->nr_written && + pack_size_limit_cfg && + pack_size_limit_cfg < state->offset + written) { +git_deflate_abort (&s); +return -1 ; + } + +hashwrite (state->f, obuf, written); + state->offset += written; + } + s.next_out = obuf; + s.avail_out =sizeof (obuf); + } + +switch (status) { +case Z_OK : +case Z_BUF_ERROR : +case Z_STREAM_END : +continue ; +default : +die ("unexpected deflate failure: %d" , status); + } + } +git_deflate_end (&s); +return 0 ; +} +/* * Read the contents from fd for size bytes, streaming it to the * packfile in state while updating the hash in ctx. Signal a failure
@@ -313,6 +376,48 @@ static void finalize_checkpoint(struct bulk_checkin_packfile *state, } } +static int deflate_obj_contents_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct object_id *result_oid, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +struct hashfile_checkpoint checkpoint = {0 }; +struct pack_idx_entry *idx =NULL ; +off_t already_hashed_to =0 ; + +/* Note: idx is non-NULL when we are writing */ +if (flags &HASH_WRITE_OBJECT ) +CALLOC_ARRAY (idx,1 ); + +while (1 ) { +prepare_checkpoint (state, &checkpoint, idx, flags); +if (!stream_obj_to_pack_incore (state, ctx, &already_hashed_to, + buf, size, type, path, flags)) +break ; +truncate_checkpoint (state, &checkpoint, idx); + } + +finalize_checkpoint (state, ctx, &checkpoint, idx, result_oid); + +return 0 ; +} + +static int deflate_blob_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; + +format_object_header_hash (the_hash_algo, &ctx,OBJ_BLOB , size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, result_oid, + buf, size,OBJ_BLOB , path, + flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -392,6 +497,17 @@ int index_blob_bulk_checkin(struct object_id *oid,return status; } +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_blob_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;
bulk-checkin.h +4 -0
@@ -13,6 +13,10 @@ int index_blob_bulk_checkin(struct object_id *oid, int fd, size_t size, const char *path, unsigned flags); +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
PATCH 6/7 bulk-checkin: introduce `index_tree_bulk_checkin_incore()`
In reply to Taylor BlauThe remaining missing piece in order to teach the merge-tree builtin how to write the contents of a merge into a pack is a function to index tree objects into a bulk-checkin pack.
This patch implements that missing piece, which is a thin wrapper around all of the functionality introduced in previous commits.
If and when Git gains support for a "compatibility" hash algorithm, the changes to support that here will be minimal. The bulk-checkin machinery will need to convert the incoming tree to compute its length under the compatibility hash, necessary to reconstruct its header. With that information (and the converted contents of the tree), the bulk-checkin machinery will have enough to keep track of the converted object's hash in order to update the compatibility mapping.
Within deflate_tree_to_pack_incore(), the changes should be limited to something like:
if (the_repository->compat_hash_algo) {
struct strbuf converted = STRBUF_INIT;
if (convert_object_file(&compat_obj,
the_repository->hash_algo,
the_repository->compat_hash_algo, ...) < 0)
die(...);
format_object_header_hash(the_repository->compat_hash_algo,
OBJ_TREE, size);
strbuf_release(&converted);
}, assuming related changes throughout the rest of the bulk-checkin machinery necessary to update the hash of the converted object, which are likewise minimal in size.
--- bulk-checkin.c | 25 +++++++++++++++++++++++++ bulk-checkin.h | 4 ++++ 2 files changed, 29 insertions(+)
bulk-checkin.c +25 -0
@@ -418,6 +418,20 @@ static int deflate_blob_to_pack_incore(struct bulk_checkin_packfile *state, flags); } +static int deflate_tree_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; + +format_object_header_hash (the_hash_algo, &ctx,OBJ_TREE , size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, result_oid, + buf, size,OBJ_TREE , path, + flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -508,6 +522,17 @@ int index_blob_bulk_checkin_incore(struct object_id *oid,return status; } +int index_tree_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_tree_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;
bulk-checkin.h +4 -0
@@ -17,6 +17,10 @@ int index_blob_bulk_checkin_incore(struct object_id *oid, const void *buf, size_t size, const char *path, unsigned flags); +int index_tree_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
Re: [PATCH 6/7] bulk-checkin: introduce `index_tree_bulk_checkin_incore()`
Quoted 15 lines · Taylor Blau
Within
deflate_tree_to_pack_incore(), the changes should be limited to something like:if (the_repository->compat_hash_algo) { struct strbuf converted = STRBUF_INIT; if (convert_object_file(&compat_obj, the_repository->hash_algo, the_repository->compat_hash_algo, ...) < 0) die(...);format_object_header_hash(the_repository->compat_hash_algo, OBJ_TREE, size); strbuf_release(&converted);
Quoted 5 lines
}
, assuming related changes throughout the rest of the bulk-checkin machinery necessary to update the hash of the converted object, which are likewise minimal in size.
So this is close. Just in case someone wants to
go down this path I want to point out that
the converted object need to have the compat hash computed over it.
Which means that the strbuf_release in your example comes a bit early.
Eric
Re: [PATCH 6/7] bulk-checkin: introduce `index_tree_bulk_checkin_incore()`
Quoted 21 lines · Eric Biederman
Quoted 14 lines · Taylor Blau
Within
deflate_tree_to_pack_incore(), the changes should be limited to something like:if (the_repository->compat_hash_algo) { struct strbuf converted = STRBUF_INIT; if (convert_object_file(&compat_obj, the_repository->hash_algo, the_repository->compat_hash_algo, ...) < 0) die(...);format_object_header_hash(the_repository->compat_hash_algo, OBJ_TREE, size); strbuf_release(&converted);Quoted 5 lines
}
, assuming related changes throughout the rest of the bulk-checkin machinery necessary to update the hash of the converted object, which are likewise minimal in size.
Quoted 5 lines
So this is close. Just in case someone wants to
go down this path I want to point out that
the converted object need to have the compat hash computed over it.Which means that the
strbuf_releasein your example comes a bit early.
Doh. You're absolutely right. Let's fix this if/when we cross that bridge ;-).
Thanks,
Taylor
PATCH 7/7 builtin/merge-tree.c: implement support for `--write-pack`
In reply to Taylor BlauWhen using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Building on the functionality introduced in previous commits, the bulk-checkin machinery now has support to write arbitrary blob and tree objects which are small enough to be held in-core. We can use this to write any blob/tree objects generated by ORT into a separate pack instead of writing them out individually as loose.
This functionality is gated behind a new --write-pack option to merge-tree that works with the (non-deprecated) --write-tree mode.
The implementation is relatively straightforward. There are two spots within the ORT mechanism where we call write_object_file(), one for content differences within blobs, and another to assemble any new trees necessary to construct the merge. In each of those locations, conditionally replace calls to write_object_file() with index_blob_bulk_checkin_incore() or index_tree_bulk_checkin_incore() depending on which kind of object we are writing.
The only remaining task is to begin and end the transaction necessary to initialize the bulk-checkin machinery, and move any new pack(s) it created into the main object store.
[^1]: Such is the case at GitHub, where we run presumptive "test merges"
on open pull requests to see whether or not we can light up the merge button green depending on whether or not the presumptive merge was conflicted.
This is done in response to a number of user-initiated events, including viewing an open pull request whose last test merge is stale with respect to the current base and tip of the pull request. As a result, merge-tree can be run very frequently on large, active repositories.
--- Documentation/git-merge-tree.txt | 4 ++ builtin/merge-tree.c | 5 ++ merge-ort.c | 43 +++++++++++---- merge-recursive.h | 1 + t/t4301-merge-tree-write-tree.sh | 93 ++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 10 deletions(-)
Documentation/git-merge-tree.txt +4 -0
@@ -69,6 +69,10 @@ OPTIONS specify a merge-base for the merge, and specifying multiple bases is currently not supported. This option is incompatible with `--stdin`. +--write-pack:: + Write any new objects into a separate packfile instead of as + individual loose objects. + [[OUTPUT]] OUTPUT ------
builtin/merge-tree.c +5 -0
@@ -18,6 +18,7 @@#include "quote.h" #include "tree.h" #include "config.h" +#include "bulk-checkin.h" static int line_termination ='\n' ;
@@ -414,6 +415,7 @@ struct merge_tree_options {int show_messages;int name_only;int use_stdin; +int write_pack; }; static int real_merge(struct merge_tree_options *o,
@@ -440,6 +442,7 @@ static int real_merge(struct merge_tree_options *o,init_merge_options (&opt, the_repository); opt.show_rename_progress =0 ; + opt.write_pack = o->write_pack; opt.branch1 = branch1; opt.branch2 = branch2;
@@ -548,6 +551,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix) &merge_base, N_("commit"), N_("specify a merge-base for the merge")), + OPT_BOOL(0, "write-pack", &o.write_pack, + N_("write new objects to a pack instead of as loose")), OPT_END() };
merge-ort.c +33 -10
88 lines not on this page · raw message
merge-recursive.h +1 -0
@@ -48,6 +48,7 @@ struct merge_options { unsigned renormalize : 1; unsigned record_conflict_msgs_as_headers : 1;const char *msg_header_prefix; + unsigned write_pack : 1;/* internal fields used by the implementation */ struct merge_options_internal *priv;
t/t4301-merge-tree-write-tree.sh +93 -0
97 lines not on this page · raw message
Signature · 1 line
2.42.0.8.g7a7e1e881e.dirty
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 3 lines · Taylor Blau
When using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Well, be it "git merge-tree" or "git merge", new loose objects tend to accumulate until "gc" kicks in, so it is not a new problem for mere mortals, is it?
As one "interesting" use case of "merge-tree" is for a Git hosting site with bare repositories to offer trial merges, without which majority of the object their repositories acquire would have been in packs pushed by their users, "Gee, loose objects consume many inodes in exchange for easier selective pruning" becomes an issue, right?
Just like it hurts performance to have too many loose object files, presumably it would also hurt performance to keep too many packs, each came from such a trial merge. Do we have a "gc" story offered for these packs created by the new feature? E.g., "once merge-tree is done creating a trial merge, we can discard the objects created in the pack, because we never expose new objects in the pack to the outside, processes running simultaneously, so instead closing the new packfile by calling flush_bulk_checkin_packfile(), we can safely unlink the temporary pack. We do not even need to spend cycles to run a gc that requires cycles to enumerate what is still reachable", or something like that?
Thanks.
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 9 lines · Junio C Hamano
Quoted 3 lines · Taylor Blau
When using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Well, be it "git merge-tree" or "git merge", new loose objects tend to accumulate until "gc" kicks in, so it is not a new problem for mere mortals, is it?
Yeah, I would definitely suspect that this is more of an issue for forges than individual Git users.
Quoted 5 lines
As one "interesting" use case of "merge-tree" is for a Git hosting site with bare repositories to offer trial merges, without which majority of the object their repositories acquire would have been in packs pushed by their users, "Gee, loose objects consume many inodes in exchange for easier selective pruning" becomes an issue, right?
Right.
Quoted 11 lines
Just like it hurts performance to have too many loose object files, presumably it would also hurt performance to keep too many packs, each came from such a trial merge. Do we have a "gc" story offered for these packs created by the new feature? E.g., "once merge-tree is done creating a trial merge, we can discard the objects created in the pack, because we never expose new objects in the pack to the outside, processes running simultaneously, so instead closing the new packfile by calling
flush_bulk_checkin_packfile(), we can safely unlink the temporary pack. We do not even need to spend cycles to run a gc that requires cycles to enumerate what is still reachable", or something like that?
I know Johannes worked on something like this recently. IIRC, it effectively does something like:
struct tmp_objdir *tmp_objdir = tmp_objdir_create(...);
tmp_objdir_replace_primary_odb(tmp_objdir, 1);at the beginning of a merge operation, and:
tmp_objdir_discard_objects(tmp_objdir);
at the end. I haven't followed that work off-list very closely, but it is only possible for GitHub to discard certain niche kinds of merges/rebases, since in general we make the objects created during test merges available via refs/pull/N/{merge,rebase}.
I think that like anything, this is a trade-off. Having lots of packs can be a performance hindrance just like having lots of loose objects. But since we can represent more objects with fewer inodes when packed, storing those objects together in a pack is preferable when (a) you're doing lots of test-merges, and (b) you want to keep those objects around, e.g., because they are reachable.
Thanks,
Taylor
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 8 lines · Taylor Blau
Quoted 6 lines · Junio C Hamano
Quoted 3 lines · Taylor Blau
When using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Quoted 6 lines
Quoted 3 lines
Well, be it "git merge-tree" or "git merge", new loose objects tend to accumulate until "gc" kicks in, so it is not a new problem for mere mortals, is it?
Yeah, I would definitely suspect that this is more of an issue for forges than individual Git users.
It may still be nice to also do this optimization for plain "git merge" as well. I had it in my list of ideas somewhere to do a "fast-import-like" thing to avoid writing loose objects, as I suspected that'd actually be a performance impediment.
Quoted 26 lines
Quoted 5 lines
As one "interesting" use case of "merge-tree" is for a Git hosting site with bare repositories to offer trial merges, without which majority of the object their repositories acquire would have been in packs pushed by their users, "Gee, loose objects consume many inodes in exchange for easier selective pruning" becomes an issue, right?
Right.
Quoted 11 lines
Just like it hurts performance to have too many loose object files, presumably it would also hurt performance to keep too many packs, each came from such a trial merge. Do we have a "gc" story offered for these packs created by the new feature? E.g., "once merge-tree is done creating a trial merge, we can discard the objects created in the pack, because we never expose new objects in the pack to the outside, processes running simultaneously, so instead closing the new packfile by calling
flush_bulk_checkin_packfile(), we can safely unlink the temporary pack. We do not even need to spend cycles to run a gc that requires cycles to enumerate what is still reachable", or something like that?I know Johannes worked on something like this recently. IIRC, it effectively does something like:
struct tmp_objdir *tmp_objdir = tmp_objdir_create(...); tmp_objdir_replace_primary_odb(tmp_objdir, 1);
Quoted 8 lines
at the beginning of a merge operation, and:
tmp_objdir_discard_objects(tmp_objdir);at the end. I haven't followed that work off-list very closely, but it is only possible for GitHub to discard certain niche kinds of merges/rebases, since in general we make the objects created during test merges available via refs/pull/N/{merge,rebase}.
Oh, at the contributor summit, Johannes said he only needed pass/fail, not the actual commits, which is why I suggested this route. If you need to keep the actual commits, then this won't help.
I was interested in the same question as Junio, but from a different angle. fast-import documentation points out that the packs it creates are suboptimal with poorer delta choices. Are the packs created by bulk-checkin prone to the same issues? When I was thinking in terms of having "git merge" use fast-import for pack creation instead of writing loose objects (an idea I never investigated very far), I was wondering if I'd need to mark those packs as "less optimal" and do something to make sure they were more likely to be repacked.
I believe geometric repacking didn't exist back when I was thinking about this, and perhaps geometric repacking automatically handles things nicely for us. Does it, or are we risking retaining sub-optimal deltas from the bulk-checkin code?
(I've never really cracked open the pack code, so I have absolutely no idea; I'm just curious.)
Quoted 6 lines
I think that like anything, this is a trade-off. Having lots of packs can be a performance hindrance just like having lots of loose objects. But since we can represent more objects with fewer inodes when packed, storing those objects together in a pack is preferable when (a) you're doing lots of test-merges, and (b) you want to keep those objects around, e.g., because they are reachable.
A couple of the comments earlier in the series suggested this was about streaming blobs to a pack in the bulk checkin code. Are tree and commit objects also put in the pack, or will those continue to be written loosely?
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 13 lines · Elijah Newren
Quoted 12 lines · Taylor Blau
Quoted 9 lines · Junio C Hamano
Quoted 3 lines · Taylor Blau
When using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Well, be it "git merge-tree" or "git merge", new loose objects tend to accumulate until "gc" kicks in, so it is not a new problem for mere mortals, is it?
Quoted 7 lines
Quoted 2 lines
Yeah, I would definitely suspect that this is more of an issue for forges than individual Git users.
It may still be nice to also do this optimization for plain "git merge" as well. I had it in my list of ideas somewhere to do a "fast-import-like" thing to avoid writing loose objects, as I suspected that'd actually be a performance impediment.
I think that would be worth doing, definitely. I do worry a little bit about locking in low-quality deltas (or lack thereof), but more on that below...
Quoted 3 lines
Oh, at the contributor summit, Johannes said he only needed pass/fail, not the actual commits, which is why I suggested this route. If you need to keep the actual commits, then this won't help.
Yep, agreed. Like I said earlier, I think there are some niche scenarios where we just care about "would this merge cleanly?", but in most other cases we want to keep around the actual tree.
Quoted 9 lines
I was interested in the same question as Junio, but from a different angle. fast-import documentation points out that the packs it creates are suboptimal with poorer delta choices. Are the packs created by bulk-checkin prone to the same issues? When I was thinking in terms of having "git merge" use fast-import for pack creation instead of writing loose objects (an idea I never investigated very far), I was wondering if I'd need to mark those packs as "less optimal" and do something to make sure they were more likely to be repacked.
Quoted 7 lines
I believe geometric repacking didn't exist back when I was thinking about this, and perhaps geometric repacking automatically handles things nicely for us. Does it, or are we risking retaining sub-optimal deltas from the bulk-checkin code?
(I've never really cracked open the pack code, so I have absolutely no idea; I'm just curious.)
Yes, the bulk-checkin mechanism suffers from an even worse problem which is the pack it creates will contain no deltas whatsoever. The contents of the pack are just getting written as-is, so there's no fancy delta-ficiation going on.
I think Michael Haggerty (?) suggested to me off-list that it might be interesting to have a flag that we could mark packs with bad/no deltas as such so that we don't implicitly trust their contents as having high quality deltas.
Quoted 11 lines
Quoted 6 lines
I think that like anything, this is a trade-off. Having lots of packs can be a performance hindrance just like having lots of loose objects. But since we can represent more objects with fewer inodes when packed, storing those objects together in a pack is preferable when (a) you're doing lots of test-merges, and (b) you want to keep those objects around, e.g., because they are reachable.
A couple of the comments earlier in the series suggested this was about streaming blobs to a pack in the bulk checkin code. Are tree and commit objects also put in the pack, or will those continue to be written loosely?
This covers both blobs and trees, since IIUC that's all we'd need to implement support for merge-tree to be able to write any objects it creates into a pack. AFAIK merge-tree never generates any commit objects. But teaching 'merge' to perform the same bulk-checkin trick would just require us implementing index_bulk_commit_checkin_in_core() or similar, which is straightforward to do on top of the existing code.
Thanks,
Taylor
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 14 lines · Taylor Blau
Quoted 14 lines
I was interested in the same question as Junio, but from a different angle. fast-import documentation points out that the packs it creates are suboptimal with poorer delta choices. Are the packs created by bulk-checkin prone to the same issues? When I was thinking in terms of having "git merge" use fast-import for pack creation instead of writing loose objects (an idea I never investigated very far), I was wondering if I'd need to mark those packs as "less optimal" and do something to make sure they were more likely to be repacked.
I believe geometric repacking didn't exist back when I was thinking about this, and perhaps geometric repacking automatically handles things nicely for us. Does it, or are we risking retaining sub-optimal deltas from the bulk-checkin code?
Quoted 7 lines
Quoted 2 lines
(I've never really cracked open the pack code, so I have absolutely no idea; I'm just curious.)
Yes, the bulk-checkin mechanism suffers from an even worse problem which is the pack it creates will contain no deltas whatsoever. The contents of the pack are just getting written as-is, so there's no fancy delta-ficiation going on.
I wonder how big a deal this would be in practice for merges. pack-objects will look for deltas between any two candidates objects, but in practice I think most deltas are between objects from multiple commits (across the "time" dimension, if you will) rather than within a single tree (the "space" dimension). And a merge operation is generally creating a single new tree (recursive merging may create intermediate states which would delta, but we don't actually need to keep those intermediate ones. I won't be surprised if we do, though).
We should be able to test that theory by looking at existing deltas. Here's a script which builds an index of blobs and trees to the commits that introduce them:
git rev-list HEAD |
git diff-tree --stdin -r -m -t --raw |
perl -lne '
if (/^[0-9a-f]/) {
$commit = $_;
} elsif (/^:\S+ \S+ \S+ (\S+)/) {
$h{$1} = $commit;
}
END { print "$_ $h{$_}" for keys(%h) }
' >commits.dbAnd then we can see which deltas come from the same commit:
git cat-file --batch-all-objects --batch-check='%(objectname) %(deltabase)' |
perl -alne '
BEGIN {
open(my $fh, "<", "commits.db");
%commit = map { chomp; split } <$fh>;
}
next if $F[1] =~ /0{40}/; # not a delta
next unless defined $commit{$F[0]}; # not in index
print $commit{$F[0]} eq $commit{$F[1]} ? "inner" : "outer", " ", $_;
'In git.git, I see 460 "inner" deltas, and 193,081 "outer" ones. The inner ones are mostly small single-file test vectors, which makes sense. It's possible to have a merge result that does conflict resolution in two such files (that would then delta), but it seems like a fairly unlikely case. Numbers for linux.git are similar.
So it might just not be a big issue at all for this use case.
Quoted 4 lines
I think Michael Haggerty (?) suggested to me off-list that it might be interesting to have a flag that we could mark packs with bad/no deltas as such so that we don't implicitly trust their contents as having high quality deltas.
I was going to suggest the same thing. ;) Unfortunately it's a bit tricky to do as we have no room in the file format for an optional flag. You'd have to add a ".mediocre-delta" file or something.
But here's another approach. I recall discussing a while back the idea that we should not necessarily trust the quality of deltas in packs that are pushed (and I think Thomas Gummerer even did some experiments inside GitHub with those, though I don't remember the results). And one way around that is during geometric repacking to consider the biggest/oldest pack as "preferred", reuse its deltas, but always compute from scratch with the others (neither reusing on-disk deltas, nor skipping try_delta() when two objects come from the same pack).
That same strategy would work here (and for incremental fast-import packs, though of course not if your fast-import pack is the "big" one after you do a from-scratch import).
Quoted 11 lines
Quoted 4 lines
A couple of the comments earlier in the series suggested this was about streaming blobs to a pack in the bulk checkin code. Are tree and commit objects also put in the pack, or will those continue to be written loosely?
This covers both blobs and trees, since IIUC that's all we'd need to implement support for merge-tree to be able to write any objects it creates into a pack. AFAIK merge-tree never generates any commit objects. But teaching 'merge' to perform the same bulk-checkin trick would just require us implementing
index_bulk_commit_checkin_in_core()or similar, which is straightforward to do on top of the existing code.
This is a bit of a devil's advocate question, but: would it make sense to implement this as a general of Git's object-writing code, and not tie it to a specific command? That is, what if a user could do:
git --write-to-pack merge ...
but also:
git --write-to-pack add ...
and the object-writing code would just write to a pack instead of writing loose objects. That lets the caller decide when it is or is not a good idea to use this mode. And if making loose objects gives bad performance for merges, wouldn't the same be true of other operations which generate many objects?
Possibly it exacerbates the "no deltas" issue from above (though it would depend on the command). The bigger question to me is one of checkpointing. When do we finish off the pack with a .idx and make it available to other readers? We could do it at program exit, but I suspect there are some commands that really want to make objects available sooner (e.g., as soon as "git add" writes an index, we'd want those objects to already be available). Probably every program that writes objects would need to be annotated with a checkpoint call (which would be a noop in loose object mode).
So maybe it's a dumb direction. I dunno.
-Peff
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 61 lines · Jeff King
Quoted 14 lines · Taylor Blau
Quoted 14 lines
I was interested in the same question as Junio, but from a different angle. fast-import documentation points out that the packs it creates are suboptimal with poorer delta choices. Are the packs created by bulk-checkin prone to the same issues? When I was thinking in terms of having "git merge" use fast-import for pack creation instead of writing loose objects (an idea I never investigated very far), I was wondering if I'd need to mark those packs as "less optimal" and do something to make sure they were more likely to be repacked.
I believe geometric repacking didn't exist back when I was thinking about this, and perhaps geometric repacking automatically handles things nicely for us. Does it, or are we risking retaining sub-optimal deltas from the bulk-checkin code?
Quoted 7 lines
Quoted 2 lines
(I've never really cracked open the pack code, so I have absolutely no idea; I'm just curious.)
Yes, the bulk-checkin mechanism suffers from an even worse problem which is the pack it creates will contain no deltas whatsoever. The contents of the pack are just getting written as-is, so there's no fancy delta-ficiation going on.
I wonder how big a deal this would be in practice for merges. pack-objects will look for deltas between any two candidates objects, but in practice I think most deltas are between objects from multiple commits (across the "time" dimension, if you will) rather than within a single tree (the "space" dimension). And a merge operation is generally creating a single new tree (recursive merging may create intermediate states which would delta, but we don't actually need to keep those intermediate ones. I won't be surprised if we do, though).
We should be able to test that theory by looking at existing deltas. Here's a script which builds an index of blobs and trees to the commits that introduce them:
git rev-list HEAD | git diff-tree --stdin -r -m -t --raw | perl -lne ' if (/^[0-9a-f]/) { $commit = $_; } elsif (/^:\S+ \S+ \S+ (\S+)/) { $h{$1} = $commit; } END { print "$_ $h{$_}" for keys(%h) } ' >commits.dbAnd then we can see which deltas come from the same commit:
git cat-file --batch-all-objects --batch-check='%(objectname) %(deltabase)' | perl -alne ' BEGIN { open(my $fh, "<", "commits.db"); %commit = map { chomp; split } <$fh>; } next if $F[1] =~ /0{40}/; # not a delta next unless defined $commit{$F[0]}; # not in index print $commit{$F[0]} eq $commit{$F[1]} ? "inner" : "outer", " ", $_; '
Quoted 7 lines
In git.git, I see 460 "inner" deltas, and 193,081 "outer" ones. The inner ones are mostly small single-file test vectors, which makes sense. It's possible to have a merge result that does conflict resolution in two such files (that would then delta), but it seems like a fairly unlikely case. Numbers for linux.git are similar.
So it might just not be a big issue at all for this use case.
Very interesting, thanks for running (and documenting!) this experiment. I'm mostly with you that it probably doesn't make a huge difference in practice here.
One thing that I'm not entirely clear on is how we'd treat objects that could be good delta candidates for each other between two packs. For instance, if I write a tree corresponding to the merge between two branches, it's likely that the resulting tree would be a good delta candidate against either of the trees at the tips of those two refs.
But we won't pack those trees (the ones at the tips of the refs) in the same pack as the tree containing their merge. If we later on tried to repack, would we evaluate the tip trees as possible delta candidates against the merged tree? Or would we look at the merged tree, realize it isn't delta'd with anything, and then not attempt to find any candidates?
Quoted 8 lines
Quoted 4 lines
I think Michael Haggerty (?) suggested to me off-list that it might be interesting to have a flag that we could mark packs with bad/no deltas as such so that we don't implicitly trust their contents as having high quality deltas.
I was going to suggest the same thing. ;) Unfortunately it's a bit tricky to do as we have no room in the file format for an optional flag. You'd have to add a ".mediocre-delta" file or something.
Yeah, I figured that we'd add a new ".baddeltas" file or something. (As an aside, we probably should have an optional flags section in the .pack format, since we seem to have a lot of optional pack extensions: .rev, .bitmap, .keep, .promisor, etc.)
Quoted 9 lines
But here's another approach. I recall discussing a while back the idea that we should not necessarily trust the quality of deltas in packs that are pushed (and I think Thomas Gummerer even did some experiments inside GitHub with those, though I don't remember the results). And one way around that is during geometric repacking to consider the biggest/oldest pack as "preferred", reuse its deltas, but always compute from scratch with the others (neither reusing on-disk deltas, nor skipping
try_delta()when two objects come from the same pack).
Quoted 3 lines
That same strategy would work here (and for incremental fast-import packs, though of course not if your fast-import pack is the "big" one after you do a from-scratch import).
Yeah, definitely. I think that that code is still living in GitHub's fork, but inactive since we haven't set any of the relevant configuration in GitHub's production environment.
Quoted 11 lines
Possibly it exacerbates the "no deltas" issue from above (though it would depend on the command). The bigger question to me is one of checkpointing. When do we finish off the pack with a .idx and make it available to other readers? We could do it at program exit, but I suspect there are some commands that really want to make objects available sooner (e.g., as soon as "git add" writes an index, we'd want those objects to already be available). Probably every program that writes objects would need to be annotated with a checkpoint call (which would be a noop in loose object mode).
So maybe it's a dumb direction. I dunno.
I wouldn't say it's a dumb direction ;-). But I'd be hesitant pursuing it without solving the "no deltas" question from earlier.
Thanks,
Taylor
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 10 lines · Taylor Blau
Very interesting, thanks for running (and documenting!) this experiment. I'm mostly with you that it probably doesn't make a huge difference in practice here.
One thing that I'm not entirely clear on is how we'd treat objects that could be good delta candidates for each other between two packs. For instance, if I write a tree corresponding to the merge between two branches, it's likely that the resulting tree would be a good delta candidate against either of the trees at the tips of those two refs.
Quoted 6 lines
But we won't pack those trees (the ones at the tips of the refs) in the same pack as the tree containing their merge. If we later on tried to repack, would we evaluate the tip trees as possible delta candidates against the merged tree? Or would we look at the merged tree, realize it isn't delta'd with anything, and then not attempt to find any candidates?
When we repack (either all-into-one, or in a geometric roll-up), we should consider those trees as candidates. The only deltas we don't consider are:
- if something is already a delta in a pack, then we will usually reuse that delta verbatim (so you might get fooled by a mediocre delta and not go to the trouble to search again. But I don't think that applies here; there is no other tree in your new pack to make such a mediocre delta from, and anyway you are skipping deltas entirely)
- if two objects are in the same pack but there's no delta relationship, the try_delta() heuristics will skip them immediately (under the assumption that we tried during the last repack and didn't find anything good).
So yes, if you had a big old pack with the original trees, and then a new pack with the merge result, we should try to delta the new merge result tree against the others, just as we would if it were loose.
Quoted 8 lines
Quoted 3 lines
I was going to suggest the same thing. ;) Unfortunately it's a bit tricky to do as we have no room in the file format for an optional flag. You'd have to add a ".mediocre-delta" file or something.
Yeah, I figured that we'd add a new ".baddeltas" file or something. (As an aside, we probably should have an optional flags section in the .pack format, since we seem to have a lot of optional pack extensions: .rev, .bitmap, .keep, .promisor, etc.)
Yes, though since packv2 is the on-the-wire format, it's very hard to change now. It might be easier to put an annotation into the .idx file.
-Peff
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
In reply to Jeff KingQuoted 7 lines · Jeff King
Quoted 4 lines
Yes, the bulk-checkin mechanism suffers from an even worse problem which is the pack it creates will contain no deltas whatsoever. The contents of the pack are just getting written as-is, so there's no fancy delta-ficiation going on.
I wonder how big a deal this would be in practice for merges.
...
Thanks for your experiments ;-)
The reason why bulk-checkin mechanism does not attempt deltifying (as opposed to fast-import that attempts to deltify with the immediately previous object and only with that single object) is exactly the same. It was done to support the initial check-in, which by definition lacks the delta opportunity along the time axis.
As you describe, such a delta-less pack would risk missed deltification opportunity when running a repack (without "-f"), as the opposite of the well known "reuse delta" heuristics, aka "this object was stored in the base form, it is likely that the previous pack-object tried but did not find a good delta base for it, let's not waste time retrying that" heuristics would get in the way.
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
In reply to Taylor BlauQuoted 49 lines · Taylor Blau
Quoted 9 lines · Junio C Hamano
Quoted 3 lines · Taylor Blau
When using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Well, be it "git merge-tree" or "git merge", new loose objects tend to accumulate until "gc" kicks in, so it is not a new problem for mere mortals, is it?
Yeah, I would definitely suspect that this is more of an issue for forges than individual Git users.
Quoted 5 lines
As one "interesting" use case of "merge-tree" is for a Git hosting site with bare repositories to offer trial merges, without which majority of the object their repositories acquire would have been in packs pushed by their users, "Gee, loose objects consume many inodes in exchange for easier selective pruning" becomes an issue, right?
Right.
Quoted 11 lines
Just like it hurts performance to have too many loose object files, presumably it would also hurt performance to keep too many packs, each came from such a trial merge. Do we have a "gc" story offered for these packs created by the new feature? E.g., "once merge-tree is done creating a trial merge, we can discard the objects created in the pack, because we never expose new objects in the pack to the outside, processes running simultaneously, so instead closing the new packfile by calling
flush_bulk_checkin_packfile(), we can safely unlink the temporary pack. We do not even need to spend cycles to run a gc that requires cycles to enumerate what is still reachable", or something like that?I know Johannes worked on something like this recently. IIRC, it effectively does something like:
struct tmp_objdir *tmp_objdir = tmp_objdir_create(...); tmp_objdir_replace_primary_odb(tmp_objdir, 1);at the beginning of a merge operation, and:
tmp_objdir_discard_objects(tmp_objdir);at the end. I haven't followed that work off-list very closely, but it is only possible for GitHub to discard certain niche kinds of merges/rebases, since in general we make the objects created during test merges available via refs/pull/N/{merge,rebase}.
Quoted 6 lines
I think that like anything, this is a trade-off. Having lots of packs can be a performance hindrance just like having lots of loose objects. But since we can represent more objects with fewer inodes when packed, storing those objects together in a pack is preferable when (a) you're doing lots of test-merges, and (b) you want to keep those objects around, e.g., because they are reachable.
In Gitaly, we usually set up quarantine directories for all operations that create objects. This allows us to discard any newly written objects in case either the RPC call gets cancelled or in case our access checks determine that the change should not be allowed. The logic is rather simple:
1. Create a new temporary directory.
2. Set up the new temporary directory as main object database via the GIT_OBJECT_DIRECTORY environment variable.
3. Set up the main repository's object database via the GIT_ALTERNATE_OBJECT_DIRECTORIES environment variable.
4. Execute Git commands that write objects with these environment variables set up. The new objects will end up neatly contained in the temporary directory.
5. Once done, either discard the temporary object database or migrate objects into the main object daatabase.
I wonder whether this would be a viable approach for you, as well.
Patrick
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 6 lines · Patrick Steinhardt
In Gitaly, we usually set up quarantine directories for all operations that create objects. This allows us to discard any newly written objects in case either the RPC call gets cancelled or in case our access checks determine that the change should not be allowed. The logic is rather simple:
Quoted 7 lines
1. Create a new temporary directory.
2. Set up the new temporary directory as main object database via the
GIT_OBJECT_DIRECTORYenvironment variable.3. Set up the main repository's object database via the
GIT_ALTERNATE_OBJECT_DIRECTORIESenvironment variable.
Is there a reason not to run Git in the quarantine environment and list the main repository as an alternate via $GIT_DIR/objects/info/alternates instead of the GIT_ALTERNATE_OBJECT_DIRECTORIES environment variable?
Quoted 6 lines
4. Execute Git commands that write objects with these environment variables set up. The new objects will end up neatly contained in the temporary directory.
5. Once done, either discard the temporary object database or migrate objects into the main object daatabase.
Interesting. I'm curious why you don't use the builtin tmp_objdir mechanism in Git itself. Do you need to run more than one command in the quarantine environment? If so, that makes sense that you'd want to have a scratch repository that lasts beyond the lifetime of a single process.
Quoted 1 line
I wonder whether this would be a viable approach for you, as well.
I think that the main problem that we are trying to solve with this series is creating a potentially large number of loose objects. I think that you could do something like what you propose above, with a 'git repacks -adk' before moving its objects over back to the main repository.
But since we're working in a single process only when doing a merge-tree operation, I think it is probably more expedient to write the pack's bytes directly.
Quoted 1 line
Patrick
Thanks,
Taylor
Re: [PATCH 7/7] builtin/merge-tree.c: implement support for `--write-pack`
Quoted 12 lines · Taylor Blau
Quoted 11 lines · Patrick Steinhardt
In Gitaly, we usually set up quarantine directories for all operations that create objects. This allows us to discard any newly written objects in case either the RPC call gets cancelled or in case our access checks determine that the change should not be allowed. The logic is rather simple:
1. Create a new temporary directory.
2. Set up the new temporary directory as main object database via the
GIT_OBJECT_DIRECTORYenvironment variable.
Quoted 6 lines
Quoted 2 lines
3. Set up the main repository's object database via the
GIT_ALTERNATE_OBJECT_DIRECTORIESenvironment variable.Is there a reason not to run Git in the quarantine environment and list the main repository as an alternate via $GIT_DIR/objects/info/alternates instead of the
GIT_ALTERNATE_OBJECT_DIRECTORIESenvironment variable?
The quarantine environment as we use it is really only a single object database, so you cannot run Git inside of it directly.
Quoted 11 lines
Quoted 6 lines
4. Execute Git commands that write objects with these environment variables set up. The new objects will end up neatly contained in the temporary directory.
5. Once done, either discard the temporary object database or migrate objects into the main object daatabase.
Interesting. I'm curious why you don't use the builtin
tmp_objdirmechanism in Git itself. Do you need to run more than one command in the quarantine environment? If so, that makes sense that you'd want to have a scratch repository that lasts beyond the lifetime of a single process.
It's a mixture of things:
- Many commands simply don't set up a temporary object directory.
- We want to check the result after the objects have been generated. Many of the commands don't provide hooks to do so in a reasonable way. So we want to check the result _after_ the command has exited already, and objects should not yet have been migrated into the target object database at that point.
- Sometimes we indeed want to run multiple Git commands. We use this e.g. for worktreeless rebases, where we run a succession of commands to rebase every single commit.
So ultimately, our own quarantine directory sits at a conceptually higher level than what Git commands would be able to provide.
Quoted 6 lines
Quoted 1 line
I wonder whether this would be a viable approach for you, as well.
I think that the main problem that we are trying to solve with this series is creating a potentially large number of loose objects. I think that you could do something like what you propose above, with a 'git repacks -adk' before moving its objects over back to the main repository.
Yeah, that's certainly possible. I had the feeling that there are two different problems that we're trying to solve in this thread:
- The problem that the objects are of temporary nature. Regardless of whether they are in a packfile or loose, it doesn't make much sense to put them into the main object database as they would be unreachable anyway and thus get pruned after some time.
- The problem that writing many loose objects can be slow.
My proposal only addresses the first problem.
Quoted 3 lines
But since we're working in a single process only when doing a merge-tree operation, I think it is probably more expedient to write the pack's bytes directly.
If it's about efficiency and not about how we discard those objects after the command has ran to completion then yes, I agree.
Patrick
v2 cover merge-ort: implement support for packing objects together
In reply to Taylor Blau(Previously based on 'eb/limit-bulk-checkin-to-blobs', which has since been merged. This series is now based on the tip of 'master', which is a9ecda2788 (The eighteenth batch, 2023-10-13) at the time of writing).
This series implements support for a new merge-tree option, --write-pack, which causes any newly-written objects to be packed together instead of being stored individually as loose.
Much is unchanged since last time, except for a small tweak to one of the commit messages in response to feedback from Eric W. Biederman. The series has also been rebased onto 'master', which had a couple of conflicts that I resolved pertaining to:
- 9eb5419799 (bulk-checkin: only support blobs in index_bulk_checkin,
2023-09-26)
- e0b8c84240 (treewide: fix various bugs w/ OpenSSL 3+ EVP API,
2023-09-01)They were mostly trivial resolutions, and the results can be viewed in the range-diff included below.
(From last time: the motivating use-case behind these changes is to better support repositories who invoke merge-tree frequently, generating a potentially large number of loose objects, resulting in a possible adverse effect on performance.)
Thanks in advance for any review!
Taylor Blau (7):
bulk-checkin: factor out format_object_header_hash()
bulk-checkin: factor out prepare_checkpoint()
bulk-checkin: factor out truncate_checkpoint()
bulk-checkin: factor our finalize_checkpoint()
bulk-checkin: introduce index_blob_bulk_checkin_incore()
bulk-checkin: introduce index_tree_bulk_checkin_incore() builtin/merge-tree.c: implement support for --write-pack
Documentation/git-merge-tree.txt | 4 + builtin/merge-tree.c | 5 + bulk-checkin.c | 258 ++++++++++++++++++++++++++----- bulk-checkin.h | 8 + merge-ort.c | 42 +++-- merge-recursive.h | 1 + t/t4301-merge-tree-write-tree.sh | 93 +++++++++++ 7 files changed, 363 insertions(+), 48 deletions(-)
Range-diff against v1:
1: 37f4072815 ! 1: edf1cbafc1 bulk-checkin: factor out format_object_header_hash()
@@ bulk-checkin.c: static void prepare_to_stream(struct bulk_checkin_packfile *stat
}
+static void format_object_header_hash(const struct git_hash_algo *algop,
-+ git_hash_ctx *ctx, enum object_type type,
++ git_hash_ctx *ctx,
++ struct hashfile_checkpoint *checkpoint,
++ enum object_type type,
+ size_t size)
+{
+ unsigned char header[16384];
@@ bulk-checkin.c: static void prepare_to_stream(struct bulk_checkin_packfile *stat
+
+ algop->init_fn(ctx);
+ algop->update_fn(ctx, header, header_len);
++ algop->init_fn(&checkpoint->ctx);
+}
+
static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,
@@ bulk-checkin.c: static int deflate_blob_to_pack(struct bulk_checkin_packfile *st
- OBJ_BLOB, size);
- the_hash_algo->init_fn(&ctx);
- the_hash_algo->update_fn(&ctx, obuf, header_len);
-+ format_object_header_hash(the_hash_algo, &ctx, OBJ_BLOB, size);
+- the_hash_algo->init_fn(&checkpoint.ctx);
++ format_object_header_hash(the_hash_algo, &ctx, &checkpoint, OBJ_BLOB,
++ size);
/* Note: idx is non-NULL when we are writing */
if ((flags & HASH_WRITE_OBJECT) != 0)2: 9cc1f3014a ! 2: b3f89d5853 bulk-checkin: factor out prepare_checkpoint()
@@ Commit message
## bulk-checkin.c ##
@@ bulk-checkin.c: static void format_object_header_hash(const struct git_hash_algo *algop,
- algop->update_fn(ctx, header, header_len);
+ algop->init_fn(&checkpoint->ctx);
}
+static void prepare_checkpoint(struct bulk_checkin_packfile *state,
3: f392ed2211 = 3: abe4fb0a59 bulk-checkin: factor out `truncate_checkpoint()`
4: 9c6ca564ad = 4: 0b855a6eb7 bulk-checkin: factor our `finalize_checkpoint()`
5: 30ca7334c7 ! 5: 239bf39bfb bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
@@ bulk-checkin.c: static void finalize_checkpoint(struct bulk_checkin_packfile *st
+static int deflate_obj_contents_to_pack_incore(struct bulk_checkin_packfile *state,
+ git_hash_ctx *ctx,
++ struct hashfile_checkpoint *checkpoint,
+ struct object_id *result_oid,
+ const void *buf, size_t size,
+ enum object_type type,
+ const char *path, unsigned flags)
+{
-+ struct hashfile_checkpoint checkpoint = {0};
+ struct pack_idx_entry *idx = NULL;
+ off_t already_hashed_to = 0;
+
@@ bulk-checkin.c: static void finalize_checkpoint(struct bulk_checkin_packfile *st
+ CALLOC_ARRAY(idx, 1);
+
+ while (1) {
-+ prepare_checkpoint(state, &checkpoint, idx, flags);
++ prepare_checkpoint(state, checkpoint, idx, flags);
+ if (!stream_obj_to_pack_incore(state, ctx, &already_hashed_to,
+ buf, size, type, path, flags))
+ break;
-+ truncate_checkpoint(state, &checkpoint, idx);
++ truncate_checkpoint(state, checkpoint, idx);
+ }
+
-+ finalize_checkpoint(state, ctx, &checkpoint, idx, result_oid);
++ finalize_checkpoint(state, ctx, checkpoint, idx, result_oid);
+
+ return 0;
+}
@@ bulk-checkin.c: static void finalize_checkpoint(struct bulk_checkin_packfile *st
+ const char *path, unsigned flags)
+{
+ git_hash_ctx ctx;
++ struct hashfile_checkpoint checkpoint = {0};
+
-+ format_object_header_hash(the_hash_algo, &ctx, OBJ_BLOB, size);
++ format_object_header_hash(the_hash_algo, &ctx, &checkpoint, OBJ_BLOB,
++ size);
+
-+ return deflate_obj_contents_to_pack_incore(state, &ctx, result_oid,
-+ buf, size, OBJ_BLOB, path,
-+ flags);
++ return deflate_obj_contents_to_pack_incore(state, &ctx, &checkpoint,
++ result_oid, buf, size,
++ OBJ_BLOB, path, flags);
+}
+
static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,6: cb0f79cabb ! 6: 57613807d8 bulk-checkin: introduce index_tree_bulk_checkin_incore()
@@ Commit message
Within deflate_tree_to_pack_incore(), the changes should be limited to something like:
+ struct strbuf converted = STRBUF_INIT;
if (the_repository->compat_hash_algo) {
- struct strbuf converted = STRBUF_INIT;
if (convert_object_file(&compat_obj,
the_repository->hash_algo, the_repository->compat_hash_algo, ...) < 0)
@@ Commit message
format_object_header_hash(the_repository->compat_hash_algo,
OBJ_TREE, size);
-
- strbuf_release(&converted);
}
+ /* compute the converted tree's hash using the compat algorithm */
+ strbuf_release(&converted);
, assuming related changes throughout the rest of the bulk-checkin machinery necessary to update the hash of the converted object, which
@@ Commit message
## bulk-checkin.c ##
@@ bulk-checkin.c: static int deflate_blob_to_pack_incore(struct bulk_checkin_packfile *state,
- flags);
+ OBJ_BLOB, path, flags);
}
+static int deflate_tree_to_pack_incore(struct bulk_checkin_packfile *state,
@@ bulk-checkin.c: static int deflate_blob_to_pack_incore(struct bulk_checkin_packf
+ const char *path, unsigned flags)
+{
+ git_hash_ctx ctx;
++ struct hashfile_checkpoint checkpoint = {0};
+
-+ format_object_header_hash(the_hash_algo, &ctx, OBJ_TREE, size);
++ format_object_header_hash(the_hash_algo, &ctx, &checkpoint, OBJ_TREE,
++ size);
+
-+ return deflate_obj_contents_to_pack_incore(state, &ctx, result_oid,
-+ buf, size, OBJ_TREE, path,
-+ flags);
++ return deflate_obj_contents_to_pack_incore(state, &ctx, &checkpoint,
++ result_oid, buf, size,
++ OBJ_TREE, path, flags);
+}
+
static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,
7: e969210145 ! 7: f21400f56c builtin/merge-tree.c: implement support for --write-pack
@@ merge-ort.c
* We have many arrays of size 3. Whenever we have such an array, the
@@ merge-ort.c: static int handle_content_merge(struct merge_options *opt,
if ((merge_status < 0) || !result_buf.ptr)
- ret = err(opt, _("Failed to execute internal merge"));
+ ret = error(_("failed to execute internal merge"));
- if (!ret &&
- write_object_file(result_buf.ptr, result_buf.size,
- OBJ_BLOB, &result->oid))
-- ret = err(opt, _("Unable to add %s to database"),
-- path);
+- ret = error(_("unable to add %s to database"), path);
+ if (!ret) {
+ ret = opt->write_pack
+ ? index_blob_bulk_checkin_incore(&result->oid,
@@ merge-ort.c: static int handle_content_merge(struct merge_options *opt,
+ result_buf.size,
+ OBJ_BLOB, &result->oid);
+ if (ret)
-+ ret = err(opt, _("Unable to add %s to database"),
-+ path);
++ ret = error(_("unable to add %s to database"),
++ path);
+ }
free(result_buf.ptr);Signature · 1 line
2.42.0.405.gdb2a2f287e
PATCH v2 1/7 bulk-checkin: factor out `format_object_header_hash()`
Before deflating a blob into a pack, the bulk-checkin mechanism prepares the pack object header by calling format_object_header(), and writing into a scratch buffer, the contents of which eventually makes its way into the pack.
Future commits will add support for deflating multiple kinds of objects into a pack, and will likewise need to perform a similar operation as below.
This is a mostly straightforward extraction, with one notable exception. Instead of hard-coding the_hash_algo, pass it in to the new function as an argument. This isn't strictly necessary for our immediate purposes here, but will prove useful in the future if/when the bulk-checkin mechanism grows support for the hash transition plan.
--- bulk-checkin.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-)
bulk-checkin.c +18 -7
@@ -247,6 +247,22 @@ static void prepare_to_stream(struct bulk_checkin_packfile *state,die_errno ("unable to write pack header" ); } +static void format_object_header_hash (const struct git_hash_algo *algop, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +enum object_type type, +size_t size) +{ +unsigned char header[16384 ]; +unsigned header_len =format_object_header ((char *)header, +sizeof (header), + type, size); + + algop->init_fn (ctx); + algop->update_fn (ctx, header, header_len); + algop->init_fn (&checkpoint->ctx); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -254,8 +270,6 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, {off_t seekback, already_hashed_to;git_hash_ctx ctx; -unsigned char obuf[16384 ]; -unsigned header_len;struct hashfile_checkpoint checkpoint = {0 };struct pack_idx_entry *idx =NULL ;
@@ -263,11 +277,8 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (seekback == (off_t) -1 )return error ("cannot find the current offset" ); - header_len =format_object_header ((char *)obuf,sizeof (obuf), -OBJ_BLOB , size); - the_hash_algo->init_fn (&ctx); - the_hash_algo->update_fn (&ctx, obuf, header_len); - the_hash_algo->init_fn (&checkpoint.ctx); +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_BLOB , + size);/* Note: idx is non-NULL when we are writing */ if ((flags &HASH_WRITE_OBJECT ) !=0 )
Signature · 1 line
2.42.0.405.gdb2a2f287e
PATCH v2 2/7 bulk-checkin: factor out `prepare_checkpoint()`
In reply to Taylor BlauIn a similar spirit as the previous commit, factor out the routine to prepare streaming into a bulk-checkin pack into its own function. Unlike the previous patch, this is a verbatim copy and paste.
--- bulk-checkin.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-)
bulk-checkin.c +14 -6
@@ -263,6 +263,19 @@ static void format_object_header_hash(const struct git_hash_algo *algop, algop->init_fn (&checkpoint->ctx); } +static void prepare_checkpoint (struct bulk_checkin_packfile *state, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx, +unsigned flags) +{ +prepare_to_stream (state, flags); +if (idx) { +hashfile_checkpoint (state->f, checkpoint); + idx->offset = state->offset; +crc32_begin (state->f); + } +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -287,12 +300,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, already_hashed_to =0 ;while (1 ) { -prepare_to_stream (state, flags); -if (idx) { -hashfile_checkpoint (state->f, &checkpoint); - idx->offset = state->offset; -crc32_begin (state->f); - } +prepare_checkpoint (state, &checkpoint, idx, flags);if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, fd, size, path, flags))break ;
Signature · 1 line
2.42.0.405.gdb2a2f287e
PATCH v2 3/7 bulk-checkin: factor out `truncate_checkpoint()`
In reply to Taylor BlauIn a similar spirit as previous commits, factor our the routine to truncate a bulk-checkin packfile when writing past the pack size limit.
--- bulk-checkin.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-)
bulk-checkin.c +17 -10
@@ -276,6 +276,22 @@ static void prepare_checkpoint(struct bulk_checkin_packfile *state, } } +static void truncate_checkpoint (struct bulk_checkin_packfile *state, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx) +{ +/* +* Writing this object to the current pack will make +* it too big; we need to truncate it, start a new +* pack, and write into it. +*/ +if (!idx) +BUG ("should not happen" ); +hashfile_truncate (state->f, checkpoint); + state->offset = checkpoint->offset; +flush_bulk_checkin_packfile (state); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -304,16 +320,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, fd, size, path, flags))break ; -/* -* Writing this object to the current pack will make -* it too big; we need to truncate it, start a new -* pack, and write into it. -*/ -if (!idx) -BUG ("should not happen" ); -hashfile_truncate (state->f, &checkpoint); - state->offset = checkpoint.offset; -flush_bulk_checkin_packfile (state); +truncate_checkpoint (state, &checkpoint, idx);if (lseek (fd, seekback,SEEK_SET ) == (off_t) -1 )return error ("cannot seek back" ); }
Signature · 1 line
2.42.0.405.gdb2a2f287e
PATCH v2 4/7 bulk-checkin: factor our `finalize_checkpoint()`
In reply to Taylor BlauIn a similar spirit as previous commits, factor out the routine to finalize the just-written object from the bulk-checkin mechanism.
--- bulk-checkin.c | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-)
bulk-checkin.c +25 -16
@@ -292,6 +292,30 @@ static void truncate_checkpoint(struct bulk_checkin_packfile *state,flush_bulk_checkin_packfile (state); } +static void finalize_checkpoint (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx, +struct object_id *result_oid) +{ + the_hash_algo->final_oid_fn (result_oid, ctx); +if (!idx) +return ; + + idx->crc32 =crc32_end (state->f); +if (already_written (state, result_oid)) { +hashfile_truncate (state->f, checkpoint); + state->offset = checkpoint->offset; +free (idx); + }else { +oidcpy (&idx->oid, result_oid); +ALLOC_GROW (state->written, + state->nr_written +1 , + state->alloc_written); + state->written[state->nr_written++] = idx; + } +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -324,22 +348,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (lseek (fd, seekback,SEEK_SET ) == (off_t) -1 )return error ("cannot seek back" ); } - the_hash_algo->final_oid_fn (result_oid, &ctx); -if (!idx) -return 0 ; - - idx->crc32 =crc32_end (state->f); -if (already_written (state, result_oid)) { -hashfile_truncate (state->f, &checkpoint); - state->offset = checkpoint.offset; -free (idx); - }else { -oidcpy (&idx->oid, result_oid); -ALLOC_GROW (state->written, - state->nr_written +1 , - state->alloc_written); - state->written[state->nr_written++] = idx; - } +finalize_checkpoint (state, &ctx, &checkpoint, idx, result_oid);return 0 ; }
Signature · 1 line
2.42.0.405.gdb2a2f287e
PATCH v2 5/7 bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
In reply to Taylor BlauNow that we have factored out many of the common routines necessary to index a new object into a pack created by the bulk-checkin machinery, we can introduce a variant of index_blob_bulk_checkin() that acts on blobs whose contents we can fit in memory.
This will be useful in a couple of more commits in order to provide the merge-tree builtin with a mechanism to create a new pack containing any objects it created during the merge, instead of storing those objects individually as loose.
Similar to the existing index_blob_bulk_checkin() function, the entrypoint delegates to deflate_blob_to_pack_incore(), which is responsible for formatting the pack header and then deflating the contents into the pack. The latter is accomplished by calling deflate_blob_contents_to_pack_incore(), which takes advantage of the earlier refactoring and is responsible for writing the object to the pack and handling any overage from pack.packSizeLimit.
The bulk of the new functionality is implemented in the function stream_obj_to_pack_incore(), which is a generic implementation for writing objects of arbitrary type (whose contents we can fit in-core) into a bulk-checkin pack.
The new function shares an unfortunate degree of similarity to the existing stream_blob_to_pack() function. But DRY-ing up these two would likely be more trouble than it's worth, since the latter has to deal with reading and writing the contents of the object.
Consistent with the rest of the bulk-checkin mechanism, there are no direct tests here. In future commits when we expose this new functionality via the merge-tree builtin, we will test it indirectly there.
--- bulk-checkin.c | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ bulk-checkin.h | 4 ++ 2 files changed, 122 insertions(+)
bulk-checkin.c +118 -0
@@ -140,6 +140,69 @@ static int already_written(struct bulk_checkin_packfile *state, struct object_idreturn 0 ; } +static int stream_obj_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +off_t *already_hashed_to, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +git_zstream s; +unsigned char obuf[16384 ]; +unsigned hdrlen; +int status =Z_OK ; +int write_object = (flags &HASH_WRITE_OBJECT ); + +git_deflate_init (&s, pack_compression_level); + + hdrlen =encode_in_pack_object_header (obuf,sizeof (obuf), type, size); + s.next_out = obuf + hdrlen; + s.avail_out =sizeof (obuf) - hdrlen; + +if (*already_hashed_to < size) { +size_t hsize = size - *already_hashed_to; +if (hsize) { + the_hash_algo->update_fn (ctx, buf, hsize); + } + *already_hashed_to = size; + } + s.next_in = (void *)buf; + s.avail_in = size; + +while (status !=Z_STREAM_END ) { + status =git_deflate (&s,Z_FINISH ); +if (!s.avail_out || status ==Z_STREAM_END ) { +if (write_object) { +size_t written = s.next_out - obuf; + +/* would we bust the size limit? */ +if (state->nr_written && + pack_size_limit_cfg && + pack_size_limit_cfg < state->offset + written) { +git_deflate_abort (&s); +return -1 ; + } + +hashwrite (state->f, obuf, written); + state->offset += written; + } + s.next_out = obuf; + s.avail_out =sizeof (obuf); + } + +switch (status) { +case Z_OK : +case Z_BUF_ERROR : +case Z_STREAM_END : +continue ; +default : +die ("unexpected deflate failure: %d" , status); + } + } +git_deflate_end (&s); +return 0 ; +} +/* * Read the contents from fd for size bytes, streaming it to the * packfile in state while updating the hash in ctx. Signal a failure
@@ -316,6 +379,50 @@ static void finalize_checkpoint(struct bulk_checkin_packfile *state, } } +static int deflate_obj_contents_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +struct object_id *result_oid, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +struct pack_idx_entry *idx =NULL ; +off_t already_hashed_to =0 ; + +/* Note: idx is non-NULL when we are writing */ +if (flags &HASH_WRITE_OBJECT ) +CALLOC_ARRAY (idx,1 ); + +while (1 ) { +prepare_checkpoint (state, checkpoint, idx, flags); +if (!stream_obj_to_pack_incore (state, ctx, &already_hashed_to, + buf, size, type, path, flags)) +break ; +truncate_checkpoint (state, checkpoint, idx); + } + +finalize_checkpoint (state, ctx, checkpoint, idx, result_oid); + +return 0 ; +} + +static int deflate_blob_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; +struct hashfile_checkpoint checkpoint = {0 }; + +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_BLOB , + size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, &checkpoint, + result_oid, buf, size, +OBJ_BLOB , path, flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -396,6 +503,17 @@ int index_blob_bulk_checkin(struct object_id *oid,return status; } +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_blob_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;
bulk-checkin.h +4 -0
@@ -13,6 +13,10 @@ int index_blob_bulk_checkin(struct object_id *oid, int fd, size_t size, const char *path, unsigned flags); +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Signature · 1 line
2.42.0.405.gdb2a2f287e
Re: [PATCH v2 5/7] bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
Quoted 3 lines · Taylor Blau
bulk-checkin.c | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ bulk-checkin.h | 4 ++ 2 files changed, 122 insertions(+)
Unlike the previous four, which were very clear refactoring to create reusable helper functions, this step leaves a bad aftertaste after reading twice, and I think what is disturbing is that many new lines are pretty much literally copied from stream_blob_to_pack().
I wonder if we can introduce an "input" source abstraction, that replaces "fd" and "size" (and "path" for error reporting) parameters to the stream_blob_to_pack(), so that the bulk of the implementation of stream_blob_to_pack() can call its .read() method to read bytes up to "size" from such an abstracted interface? That would be a good sized first half of this change. Then in the second half, you can add another "input" source that works with in-core "buf" and "size", whose .read() method will merely be a memcpy().
That way, we will have two functions, one for stream_obj_to_pack() that reads from an open file descriptor, and the other for stream_obj_to_pack_incore() that reads from an in-core buffer, sharing the bulk of the implementation that is extracted from the current code, which hopefully be easier to audit.
Quoted 158 lines
bulk-checkin.c +118
-0@@ -140,6 +140,69 @@ static int already_written(struct bulk_checkin_packfile *state, struct object_idreturn 0 ; } +static int stream_obj_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +off_t *already_hashed_to, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +git_zstream s; +unsigned char obuf[16384 ]; +unsigned hdrlen; +int status =Z_OK ; +int write_object = (flags &HASH_WRITE_OBJECT ); + +git_deflate_init (&s, pack_compression_level); + + hdrlen =encode_in_pack_object_header (obuf,sizeof (obuf), type, size); + s.next_out = obuf + hdrlen; + s.avail_out =sizeof (obuf) - hdrlen; + +if (*already_hashed_to < size) { +size_t hsize = size - *already_hashed_to; +if (hsize) { + the_hash_algo->update_fn (ctx, buf, hsize); + } + *already_hashed_to = size; + } + s.next_in = (void *)buf; + s.avail_in = size; + +while (status !=Z_STREAM_END ) { + status =git_deflate (&s,Z_FINISH ); +if (!s.avail_out || status ==Z_STREAM_END ) { +if (write_object) { +size_t written = s.next_out - obuf; + +/* would we bust the size limit? */ +if (state->nr_written && + pack_size_limit_cfg && + pack_size_limit_cfg < state->offset + written) { +git_deflate_abort (&s); +return -1 ; + } + +hashwrite (state->f, obuf, written); + state->offset += written; + } + s.next_out = obuf; + s.avail_out =sizeof (obuf); + } + +switch (status) { +case Z_OK : +case Z_BUF_ERROR : +case Z_STREAM_END : +continue ; +default : +die ("unexpected deflate failure: %d" , status); + } + } +git_deflate_end (&s); +return 0 ; +} +/* * Read the contents from fd for size bytes, streaming it to the * packfile in state while updating the hash in ctx. Signal a failure @@ -316,6 +379,50 @@ static void finalize_checkpoint(struct bulk_checkin_packfile *state, } } +static int deflate_obj_contents_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +struct object_id *result_oid, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +struct pack_idx_entry *idx =NULL ; +off_t already_hashed_to =0 ; + +/* Note: idx is non-NULL when we are writing */ +if (flags &HASH_WRITE_OBJECT ) +CALLOC_ARRAY (idx,1 ); + +while (1 ) { +prepare_checkpoint (state, checkpoint, idx, flags); +if (!stream_obj_to_pack_incore (state, ctx, &already_hashed_to, + buf, size, type, path, flags)) +break ; +truncate_checkpoint (state, checkpoint, idx); + } + +finalize_checkpoint (state, ctx, checkpoint, idx, result_oid); + +return 0 ; +} + +static int deflate_blob_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; +struct hashfile_checkpoint checkpoint = {0 }; + +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_BLOB , + size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, &checkpoint, + result_oid, buf, size, +OBJ_BLOB , path, flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,@@ -396,6 +503,17 @@ int index_blob_bulk_checkin(struct object_id *oid,return status; } +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_blob_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;bulk-checkin.h +4
-0@@ -13,6 +13,10 @@ int index_blob_bulk_checkin(struct object_id *oid, int fd, size_t size, const char *path, unsigned flags); +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Re: [PATCH v2 5/7] bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
Quoted 11 lines · Junio C Hamano
Quoted 3 lines · Taylor Blau
bulk-checkin.c | 118 +++++++++++++++++++++++++++++++++++++++++++++++++ bulk-checkin.h | 4 ++ 2 files changed, 122 insertions(+)Unlike the previous four, which were very clear refactoring to create reusable helper functions, this step leaves a bad aftertaste after reading twice, and I think what is disturbing is that many new lines are pretty much literally copied from
stream_blob_to_pack().
Quoted 8 lines
I wonder if we can introduce an "input" source abstraction, that replaces "fd" and "size" (and "path" for error reporting) parameters to the
stream_blob_to_pack(), so that the bulk of the implementation ofstream_blob_to_pack()can call its .read() method to read bytes up to "size" from such an abstracted interface? That would be a good sized first half of this change. Then in the second half, you can add another "input" source that works with in-core "buf" and "size", whose .read() method will merely be amemcpy().
Thanks, I like this idea. I had initially avoided it in the first couple of rounds, because the abstraction felt clunky and involved an unnecessary extra memcpy().
But having applied your suggestion here, I think that the price is well worth the result, which is that stream_blob_to_pack() does not have to be implemented twice with very subtle differences.
Thanks again for the suggestion, I'm really pleased with how it came out. Reroll coming shortly...
Thanks,
Taylor
PATCH v2 6/7 bulk-checkin: introduce `index_tree_bulk_checkin_incore()`
In reply to Taylor BlauThe remaining missing piece in order to teach the merge-tree builtin how to write the contents of a merge into a pack is a function to index tree objects into a bulk-checkin pack.
This patch implements that missing piece, which is a thin wrapper around all of the functionality introduced in previous commits.
If and when Git gains support for a "compatibility" hash algorithm, the changes to support that here will be minimal. The bulk-checkin machinery will need to convert the incoming tree to compute its length under the compatibility hash, necessary to reconstruct its header. With that information (and the converted contents of the tree), the bulk-checkin machinery will have enough to keep track of the converted object's hash in order to update the compatibility mapping.
Within deflate_tree_to_pack_incore(), the changes should be limited to something like:
struct strbuf converted = STRBUF_INIT;
if (the_repository->compat_hash_algo) {
if (convert_object_file(&compat_obj,
the_repository->hash_algo,
the_repository->compat_hash_algo, ...) < 0)
die(...);
format_object_header_hash(the_repository->compat_hash_algo,
OBJ_TREE, size);
}
/* compute the converted tree's hash using the compat algorithm */
strbuf_release(&converted);, assuming related changes throughout the rest of the bulk-checkin machinery necessary to update the hash of the converted object, which are likewise minimal in size.
--- bulk-checkin.c | 27 +++++++++++++++++++++++++++ bulk-checkin.h | 4 ++++ 2 files changed, 31 insertions(+)
bulk-checkin.c +27 -0
@@ -423,6 +423,22 @@ static int deflate_blob_to_pack_incore(struct bulk_checkin_packfile *state, OBJ_BLOB, path, flags); } +static int deflate_tree_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; +struct hashfile_checkpoint checkpoint = {0 }; + +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_TREE , + size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, &checkpoint, + result_oid, buf, size, +OBJ_TREE , path, flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -514,6 +530,17 @@ int index_blob_bulk_checkin_incore(struct object_id *oid,return status; } +int index_tree_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_tree_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;
bulk-checkin.h +4 -0
@@ -17,6 +17,10 @@ int index_blob_bulk_checkin_incore(struct object_id *oid, const void *buf, size_t size, const char *path, unsigned flags); +int index_tree_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Signature · 1 line
2.42.0.405.gdb2a2f287e
PATCH v2 7/7 builtin/merge-tree.c: implement support for `--write-pack`
In reply to Taylor BlauWhen using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Building on the functionality introduced in previous commits, the bulk-checkin machinery now has support to write arbitrary blob and tree objects which are small enough to be held in-core. We can use this to write any blob/tree objects generated by ORT into a separate pack instead of writing them out individually as loose.
This functionality is gated behind a new --write-pack option to merge-tree that works with the (non-deprecated) --write-tree mode.
The implementation is relatively straightforward. There are two spots within the ORT mechanism where we call write_object_file(), one for content differences within blobs, and another to assemble any new trees necessary to construct the merge. In each of those locations, conditionally replace calls to write_object_file() with index_blob_bulk_checkin_incore() or index_tree_bulk_checkin_incore() depending on which kind of object we are writing.
The only remaining task is to begin and end the transaction necessary to initialize the bulk-checkin machinery, and move any new pack(s) it created into the main object store.
[^1]: Such is the case at GitHub, where we run presumptive "test merges"
on open pull requests to see whether or not we can light up the merge button green depending on whether or not the presumptive merge was conflicted.
This is done in response to a number of user-initiated events, including viewing an open pull request whose last test merge is stale with respect to the current base and tip of the pull request. As a result, merge-tree can be run very frequently on large, active repositories.
--- Documentation/git-merge-tree.txt | 4 ++ builtin/merge-tree.c | 5 ++ merge-ort.c | 42 +++++++++++---- merge-recursive.h | 1 + t/t4301-merge-tree-write-tree.sh | 93 ++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 9 deletions(-)
Documentation/git-merge-tree.txt +4 -0
@@ -69,6 +69,10 @@ OPTIONS specify a merge-base for the merge, and specifying multiple bases is currently not supported. This option is incompatible with `--stdin`. +--write-pack:: + Write any new objects into a separate packfile instead of as + individual loose objects. + [[OUTPUT]] OUTPUT ------
builtin/merge-tree.c +5 -0
@@ -18,6 +18,7 @@#include "quote.h" #include "tree.h" #include "config.h" +#include "bulk-checkin.h" static int line_termination ='\n' ;
@@ -414,6 +415,7 @@ struct merge_tree_options {int show_messages;int name_only;int use_stdin; +int write_pack; }; static int real_merge(struct merge_tree_options *o,
@@ -440,6 +442,7 @@ static int real_merge(struct merge_tree_options *o,init_merge_options (&opt, the_repository); opt.show_rename_progress =0 ; + opt.write_pack = o->write_pack; opt.branch1 = branch1; opt.branch2 = branch2;
@@ -548,6 +551,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix) &merge_base, N_("commit"), N_("specify a merge-base for the merge")), + OPT_BOOL(0, "write-pack", &o.write_pack, + N_("write new objects to a pack instead of as loose")), OPT_END() };
merge-ort.c +33 -9
87 lines not on this page · raw message
merge-recursive.h +1 -0
@@ -48,6 +48,7 @@ struct merge_options { unsigned renormalize : 1; unsigned record_conflict_msgs_as_headers : 1;const char *msg_header_prefix; + unsigned write_pack : 1;/* internal fields used by the implementation */ struct merge_options_internal *priv;
t/t4301-merge-tree-write-tree.sh +93 -0
97 lines not on this page · raw message
Signature · 1 line
2.42.0.405.gdb2a2f287e
v3 cover merge-ort: implement support for packing objects together
In reply to Taylor BlauThis series implements support for a new merge-tree option, --write-pack, which causes any newly-written objects to be packed together instead of being stored individually as loose.
The notable change from last time is in response to a suggestion[1] from Junio to factor out an abstract bulk-checkin "source", which ended up reducing the duplication between a couple of functions in the earlier round by a significant degree.
Beyond that, the changes since last time can be viewed in the range-diff below. Thanks in advance for any review!
[1]: https://lore.kernel.org/git/[email protected]/
Taylor Blau (10):
bulk-checkin: factor out format_object_header_hash()
bulk-checkin: factor out prepare_checkpoint()
bulk-checkin: factor out truncate_checkpoint()
bulk-checkin: factor out finalize_checkpoint()
bulk-checkin: extract abstract bulk_checkin_source
bulk-checkin: implement SOURCE_INCORE mode for bulk_checkin_source
bulk-checkin: generify stream_blob_to_pack() for arbitrary types
bulk-checkin: introduce index_blob_bulk_checkin_incore()
bulk-checkin: introduce index_tree_bulk_checkin_incore() builtin/merge-tree.c: implement support for --write-pack
Documentation/git-merge-tree.txt | 4 + builtin/merge-tree.c | 5 + bulk-checkin.c | 288 +++++++++++++++++++++++++------ bulk-checkin.h | 8 + merge-ort.c | 42 ++++- merge-recursive.h | 1 + t/t4301-merge-tree-write-tree.sh | 93 ++++++++++ 7 files changed, 381 insertions(+), 60 deletions(-)
Range-diff against v2:
1: edf1cbafc1 = 1: 2dffa45183 bulk-checkin: factor out `format_object_header_hash()` 2: b3f89d5853 = 2: 7a10dc794a bulk-checkin: factor out `prepare_checkpoint()` 3: abe4fb0a59 = 3: 20c32d2178 bulk-checkin: factor out `truncate_checkpoint()` 4: 0b855a6eb7 ! 4: 893051d0b7 bulk-checkin: factor our `finalize_checkpoint()`
@@ Metadata
Author: Taylor Blau <[email protected]>
## Commit message ##
- bulk-checkin: factor our finalize_checkpoint()
+ bulk-checkin: factor out finalize_checkpoint()
In a similar spirit as previous commits, factor out the routine to finalize the just-written object from the bulk-checkin mechanism.
-: ---------- > 5: da52ec8380 bulk-checkin: extract abstract `bulk_checkin_source`
-: ---------- > 6: 4e9bac5bc1 bulk-checkin: implement `SOURCE_INCORE` mode for `bulk_checkin_source`
-: ---------- > 7: 04ec74e357 bulk-checkin: generify `stream_blob_to_pack()` for arbitrary types
5: 239bf39bfb ! 8: 8667b76365 bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
@@ Commit message
entrypoint delegates to `deflate_blob_to_pack_incore()`, which is
responsible for formatting the pack header and then deflating the
contents into the pack. The latter is accomplished by calling
- deflate_blob_contents_to_pack_incore(), which takes advantage of the
- earlier refactoring and is responsible for writing the object to the
+ deflate_obj_contents_to_pack_incore(), which takes advantage of the
+ earlier refactorings and is responsible for writing the object to the
pack and handling any overage from pack.packSizeLimit.
The bulk of the new functionality is implemented in the function
- `stream_obj_to_pack_incore()`, which is a generic implementation for
- writing objects of arbitrary type (whose contents we can fit in-core)
- into a bulk-checkin pack.
-
- The new function shares an unfortunate degree of similarity to the
- existing `stream_blob_to_pack()` function. But DRY-ing up these two
- would likely be more trouble than it's worth, since the latter has to
- deal with reading and writing the contents of the object.
+ `stream_obj_to_pack()`, which can handle streaming objects from memory
+ to the bulk-checkin pack as a result of the earlier refactoring.
Consistent with the rest of the bulk-checkin mechanism, there are no
direct tests here. In future commits when we expose this new
@@ Commit message
Signed-off-by: Taylor Blau <[email protected]>
## bulk-checkin.c ##
-@@ bulk-checkin.c: static int already_written(struct bulk_checkin_packfile *state, struct object_id
- return 0;
- }
-
-+static int stream_obj_to_pack_incore(struct bulk_checkin_packfile *state,
-+ git_hash_ctx *ctx,
-+ off_t *already_hashed_to,
-+ const void *buf, size_t size,
-+ enum object_type type,
-+ const char *path, unsigned flags)
-+{
-+ git_zstream s;
-+ unsigned char obuf[16384];
-+ unsigned hdrlen;
-+ int status = Z_OK;
-+ int write_object = (flags & HASH_WRITE_OBJECT);
-+
-+ git_deflate_init(&s, pack_compression_level);
-+
-+ hdrlen = encode_in_pack_object_header(obuf, sizeof(obuf), type, size);
-+ s.next_out = obuf + hdrlen;
-+ s.avail_out = sizeof(obuf) - hdrlen;
-+
-+ if (*already_hashed_to < size) {
-+ size_t hsize = size - *already_hashed_to;
-+ if (hsize) {
-+ the_hash_algo->update_fn(ctx, buf, hsize);
-+ }
-+ *already_hashed_to = size;
-+ }
-+ s.next_in = (void *)buf;
-+ s.avail_in = size;
-+
-+ while (status != Z_STREAM_END) {
-+ status = git_deflate(&s, Z_FINISH);
-+ if (!s.avail_out || status == Z_STREAM_END) {
-+ if (write_object) {
-+ size_t written = s.next_out - obuf;
-+
-+ /* would we bust the size limit? */
-+ if (state->nr_written &&
-+ pack_size_limit_cfg &&
-+ pack_size_limit_cfg < state->offset + written) {
-+ git_deflate_abort(&s);
-+ return -1;
-+ }
-+
-+ hashwrite(state->f, obuf, written);
-+ state->offset += written;
-+ }
-+ s.next_out = obuf;
-+ s.avail_out = sizeof(obuf);
-+ }
-+
-+ switch (status) {
-+ case Z_OK:
-+ case Z_BUF_ERROR:
-+ case Z_STREAM_END:
-+ continue;
-+ default:
-+ die("unexpected deflate failure: %d", status);
-+ }
-+ }
-+ git_deflate_end(&s);
-+ return 0;
-+}
-+
- /*
- * Read the contents from fd for size bytes, streaming it to the
- * packfile in state while updating the hash in ctx. Signal a failure
@@ bulk-checkin.c: static void finalize_checkpoint(struct bulk_checkin_packfile *state,
}
}
@@ bulk-checkin.c: static void finalize_checkpoint(struct bulk_checkin_packfile *st
+{
+ struct pack_idx_entry *idx = NULL;
+ off_t already_hashed_to = 0;
++ struct bulk_checkin_source source = {
++ .type = SOURCE_INCORE,
++ .buf = buf,
++ .size = size,
++ .read = 0,
++ .path = path,
++ };
+
+ /* Note: idx is non-NULL when we are writing */
+ if (flags & HASH_WRITE_OBJECT)
@@ bulk-checkin.c: static void finalize_checkpoint(struct bulk_checkin_packfile *st
+
+ while (1) {
+ prepare_checkpoint(state, checkpoint, idx, flags);
-+ if (!stream_obj_to_pack_incore(state, ctx, &already_hashed_to,
-+ buf, size, type, path, flags))
++
++ if (!stream_obj_to_pack(state, ctx, &already_hashed_to, &source,
++ type, flags))
+ break;
+ truncate_checkpoint(state, checkpoint, idx);
++ bulk_checkin_source_seek_to(&source, 0);
+ }
+
+ finalize_checkpoint(state, ctx, checkpoint, idx, result_oid);
6: 57613807d8 = 9: cba043ef14 bulk-checkin: introduce `index_tree_bulk_checkin_incore()`
7: f21400f56c = 10: ae70508037 builtin/merge-tree.c: implement support for `--write-pack`Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 1/10 bulk-checkin: factor out `format_object_header_hash()`
Before deflating a blob into a pack, the bulk-checkin mechanism prepares the pack object header by calling format_object_header(), and writing into a scratch buffer, the contents of which eventually makes its way into the pack.
Future commits will add support for deflating multiple kinds of objects into a pack, and will likewise need to perform a similar operation as below.
This is a mostly straightforward extraction, with one notable exception. Instead of hard-coding the_hash_algo, pass it in to the new function as an argument. This isn't strictly necessary for our immediate purposes here, but will prove useful in the future if/when the bulk-checkin mechanism grows support for the hash transition plan.
--- bulk-checkin.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-)
bulk-checkin.c +18 -7
@@ -247,6 +247,22 @@ static void prepare_to_stream(struct bulk_checkin_packfile *state,die_errno ("unable to write pack header" ); } +static void format_object_header_hash (const struct git_hash_algo *algop, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +enum object_type type, +size_t size) +{ +unsigned char header[16384 ]; +unsigned header_len =format_object_header ((char *)header, +sizeof (header), + type, size); + + algop->init_fn (ctx); + algop->update_fn (ctx, header, header_len); + algop->init_fn (&checkpoint->ctx); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -254,8 +270,6 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, {off_t seekback, already_hashed_to;git_hash_ctx ctx; -unsigned char obuf[16384 ]; -unsigned header_len;struct hashfile_checkpoint checkpoint = {0 };struct pack_idx_entry *idx =NULL ;
@@ -263,11 +277,8 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (seekback == (off_t) -1 )return error ("cannot find the current offset" ); - header_len =format_object_header ((char *)obuf,sizeof (obuf), -OBJ_BLOB , size); - the_hash_algo->init_fn (&ctx); - the_hash_algo->update_fn (&ctx, obuf, header_len); - the_hash_algo->init_fn (&checkpoint.ctx); +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_BLOB , + size);/* Note: idx is non-NULL when we are writing */ if ((flags &HASH_WRITE_OBJECT ) !=0 )
Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 2/10 bulk-checkin: factor out `prepare_checkpoint()`
In reply to Taylor BlauIn a similar spirit as the previous commit, factor out the routine to prepare streaming into a bulk-checkin pack into its own function. Unlike the previous patch, this is a verbatim copy and paste.
--- bulk-checkin.c | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-)
bulk-checkin.c +14 -6
@@ -263,6 +263,19 @@ static void format_object_header_hash(const struct git_hash_algo *algop, algop->init_fn (&checkpoint->ctx); } +static void prepare_checkpoint (struct bulk_checkin_packfile *state, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx, +unsigned flags) +{ +prepare_to_stream (state, flags); +if (idx) { +hashfile_checkpoint (state->f, checkpoint); + idx->offset = state->offset; +crc32_begin (state->f); + } +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -287,12 +300,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, already_hashed_to =0 ;while (1 ) { -prepare_to_stream (state, flags); -if (idx) { -hashfile_checkpoint (state->f, &checkpoint); - idx->offset = state->offset; -crc32_begin (state->f); - } +prepare_checkpoint (state, &checkpoint, idx, flags);if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, fd, size, path, flags))break ;
Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 3/10 bulk-checkin: factor out `truncate_checkpoint()`
In reply to Taylor BlauIn a similar spirit as previous commits, factor our the routine to truncate a bulk-checkin packfile when writing past the pack size limit.
--- bulk-checkin.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-)
bulk-checkin.c +17 -10
@@ -276,6 +276,22 @@ static void prepare_checkpoint(struct bulk_checkin_packfile *state, } } +static void truncate_checkpoint (struct bulk_checkin_packfile *state, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx) +{ +/* +* Writing this object to the current pack will make +* it too big; we need to truncate it, start a new +* pack, and write into it. +*/ +if (!idx) +BUG ("should not happen" ); +hashfile_truncate (state->f, checkpoint); + state->offset = checkpoint->offset; +flush_bulk_checkin_packfile (state); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -304,16 +320,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, fd, size, path, flags))break ; -/* -* Writing this object to the current pack will make -* it too big; we need to truncate it, start a new -* pack, and write into it. -*/ -if (!idx) -BUG ("should not happen" ); -hashfile_truncate (state->f, &checkpoint); - state->offset = checkpoint.offset; -flush_bulk_checkin_packfile (state); +truncate_checkpoint (state, &checkpoint, idx);if (lseek (fd, seekback,SEEK_SET ) == (off_t) -1 )return error ("cannot seek back" ); }
Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 4/10 bulk-checkin: factor out `finalize_checkpoint()`
In reply to Taylor BlauIn a similar spirit as previous commits, factor out the routine to finalize the just-written object from the bulk-checkin mechanism.
--- bulk-checkin.c | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-)
bulk-checkin.c +25 -16
@@ -292,6 +292,30 @@ static void truncate_checkpoint(struct bulk_checkin_packfile *state,flush_bulk_checkin_packfile (state); } +static void finalize_checkpoint (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +struct pack_idx_entry *idx, +struct object_id *result_oid) +{ + the_hash_algo->final_oid_fn (result_oid, ctx); +if (!idx) +return ; + + idx->crc32 =crc32_end (state->f); +if (already_written (state, result_oid)) { +hashfile_truncate (state->f, checkpoint); + state->offset = checkpoint->offset; +free (idx); + }else { +oidcpy (&idx->oid, result_oid); +ALLOC_GROW (state->written, + state->nr_written +1 , + state->alloc_written); + state->written[state->nr_written++] = idx; + } +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -324,22 +348,7 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,if (lseek (fd, seekback,SEEK_SET ) == (off_t) -1 )return error ("cannot seek back" ); } - the_hash_algo->final_oid_fn (result_oid, &ctx); -if (!idx) -return 0 ; - - idx->crc32 =crc32_end (state->f); -if (already_written (state, result_oid)) { -hashfile_truncate (state->f, &checkpoint); - state->offset = checkpoint.offset; -free (idx); - }else { -oidcpy (&idx->oid, result_oid); -ALLOC_GROW (state->written, - state->nr_written +1 , - state->alloc_written); - state->written[state->nr_written++] = idx; - } +finalize_checkpoint (state, &ctx, &checkpoint, idx, result_oid);return 0 ; }
Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 5/10 bulk-checkin: extract abstract `bulk_checkin_source`
In reply to Taylor BlauA future commit will want to implement a very similar routine as in stream_blob_to_pack() with two notable changes:
- Instead of streaming just OBJ_BLOBs, this new function may want to stream objects of arbitrary type.
- Instead of streaming the object's contents from an open file-descriptor, this new function may want to "stream" its contents from memory.
To avoid duplicating a significant chunk of code between the existing stream_blob_to_pack(), extract an abstract bulk_checkin_source. This concept currently is a thin layer of lseek() and read_in_full(), but will grow to understand how to perform analogous operations when writing out an object's contents from memory.
--- bulk-checkin.c | 61 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 8 deletions(-)
bulk-checkin.c +53 -8
@@ -140,8 +140,41 @@ static int already_written(struct bulk_checkin_packfile *state, struct object_idreturn 0 ; } +struct bulk_checkin_source { +enum {SOURCE_FILE } type; + +/* SOURCE_FILE fields */ +int fd; + +/* common fields */ +size_t size; +const char *path; +}; + +static off_t bulk_checkin_source_seek_to (struct bulk_checkin_source *source, +off_t offset) +{ +switch (source->type) { +case SOURCE_FILE : +return lseek (source->fd, offset,SEEK_SET ); +default : +BUG ("unknown bulk-checkin source: %d" , source->type); + } +} + +static ssize_t bulk_checkin_source_read (struct bulk_checkin_source *source, +void *buf,size_t nr) +{ +switch (source->type) { +case SOURCE_FILE : +return read_in_full (source->fd, buf, nr); +default : +BUG ("unknown bulk-checkin source: %d" , source->type); + } +} +/* -* Read the contents from fd for size bytes, streaming it to the +* Read the contents from 'source' for 'size' bytes, streaming it to the * packfile in state while updating the hash in ctx. Signal a failure * by returning a negative value when the resulting pack would exceed * the pack size limit and this is not the first object in the pack,
@@ -157,7 +190,7 @@ static int already_written(struct bulk_checkin_packfile *state, struct object_id*/ static int stream_blob_to_pack (struct bulk_checkin_packfile *state,git_hash_ctx *ctx,off_t *already_hashed_to, -int fd,size_t size,const char *path, +struct bulk_checkin_source *source,unsigned flags) {git_zstream s;
@@ -167,22 +200,28 @@ static int stream_blob_to_pack(struct bulk_checkin_packfile *state,int status =Z_OK ;int write_object = (flags &HASH_WRITE_OBJECT );off_t offset =0 ; +size_t size = source->size;git_deflate_init (&s, pack_compression_level); - hdrlen =encode_in_pack_object_header (obuf,sizeof (obuf),OBJ_BLOB , size); + hdrlen =encode_in_pack_object_header (obuf,sizeof (obuf),OBJ_BLOB , + size); s.next_out = obuf + hdrlen; s.avail_out =sizeof (obuf) - hdrlen;while (status !=Z_STREAM_END ) {if (size && !s.avail_in) {ssize_t rsize = size <sizeof (ibuf) ? size :sizeof (ibuf); -ssize_t read_result =read_in_full (fd, ibuf, rsize); +ssize_t read_result; + + read_result =bulk_checkin_source_read (source, ibuf, + rsize);if (read_result <0 ) -die_errno ("failed to read from '%s'" , path); +die_errno ("failed to read from '%s'" , + source->path);if (read_result != rsize)die ("failed to read %d bytes from '%s'" , - (int )rsize, path); + (int )rsize, source->path); offset += rsize;if (*already_hashed_to < offset) {size_t hsize = offset - *already_hashed_to;
@@ -325,6 +364,12 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,git_hash_ctx ctx;struct hashfile_checkpoint checkpoint = {0 };struct pack_idx_entry *idx =NULL ; +struct bulk_checkin_source source = { + .type =SOURCE_FILE , + .fd = fd, + .size = size, + .path = path, + }; seekback =lseek (fd,0 ,SEEK_CUR );if (seekback == (off_t) -1 )
@@ -342,10 +387,10 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,while (1 ) {prepare_checkpoint (state, &checkpoint, idx, flags);if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, - fd, size, path, flags)) + &source, flags))break ;truncate_checkpoint (state, &checkpoint, idx); -if (lseek (fd, seekback,SEEK_SET ) == (off_t) -1 ) +if (bulk_checkin_source_seek_to (&source, seekback) == (off_t)-1 )return error ("cannot seek back" ); }finalize_checkpoint (state, &ctx, &checkpoint, idx, result_oid);
Signature · 1 line
2.42.0.408.g97fac66ae4
Re: [PATCH v3 05/10] bulk-checkin: extract abstract `bulk_checkin_source`
Quoted 30 lines · Taylor Blau
A future commit will want to implement a very similar routine as in
stream_blob_to_pack()with two notable changes:- Instead of streaming just
OBJ_BLOBs, this new function may want to stream objects of arbitrary type.- Instead of streaming the object's contents from an open file-descriptor, this new function may want to "stream" its contents from memory.
To avoid duplicating a significant chunk of code between the existing
stream_blob_to_pack(), extract an abstractbulk_checkin_source. This concept currently is a thin layer oflseek()andread_in_full(), but will grow to understand how to perform analogous operations when writing out an object's contents from memory.Suggested-by Junio C Hamano <[email protected]>Signed-off-by Taylor Blau <[email protected]>--- bulk-checkin.c | 61 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 8 deletions(-)bulk-checkin.c +0
-0@@ -140,8 +140,41 @@ static int already_written(struct bulk_checkin_packfile *state, struct object_idreturn 0 ; }
bulk-checkin.c static int already_written(struct bulk_checkin_packfile *state, struct object_id · line 152
Quoted 10 lines
bulk-checkin.c +10
-0+struct bulk_checkin_source { +enum {SOURCE_FILE } type; + +/* SOURCE_FILE fields */ +int fd; + +/* common fields */ +size_t size; +const char *path; +};
Looks OK, even though I expected to see a bit more involved object orientation with something like
struct bulk_checkin_source {
off_t (*read)(struct bulk_checkin_source *, void *, size_t);
off_t (*seek)(struct bulk_checkin_source *, off_t);
union {
struct {
int fd;
size_t size;
const char *path;
} from_fd;
struct {
...
} incore;
} data;
};As there will only be two subclasses of this thing, it may not matter all that much right now, but it would be much nicer as your methods do not have to care about "switch (enum) { case FILE: ... }".
Re: [PATCH v3 05/10] bulk-checkin: extract abstract `bulk_checkin_source`
Quoted 18 lines · Junio C Hamano
Looks OK, even though I expected to see a bit more involved object orientation with something like
struct bulk_checkin_source { off_t (*read)(struct bulk_checkin_source *, void *, size_t); off_t (*seek)(struct bulk_checkin_source *, off_t); union { struct { int fd; size_t size; const char *path; } from_fd; struct { ... } incore; } data; };
Quoted 3 lines
As there will only be two subclasses of this thing, it may not matter all that much right now, but it would be much nicer as your methods do not have to care about "switch (enum) { case FILE: ... }".
I want to be cautious of going too far in this direction. I anticipate that "two" is probably the maximum number of kinds of sources we can reasonably expect for the foreseeable future. If that changes, it's easy enough to convert from the existing implementation to something closer to the above.
Thanks,
Taylor
Re: [PATCH v3 05/10] bulk-checkin: extract abstract `bulk_checkin_source`
Quoted 1 line · Taylor Blau
I want to be cautious of going too far in this direction.
That's fine. Thanks.
PATCH v3 6/10 bulk-checkin: implement `SOURCE_INCORE` mode for `bulk_checkin_source`
In reply to Taylor BlauContinue to prepare for streaming an object's contents directly from memory by teaching bulk_checkin_source how to perform reads and seeks based on an address in memory.
Unlike file descriptors, which manage their own offset internally, we have to keep track of how many bytes we've read out of the buffer, and make sure we don't read past the end of the buffer.
--- bulk-checkin.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-)
bulk-checkin.c +17 -1
@@ -141,11 +141,15 @@ static int already_written(struct bulk_checkin_packfile *state, struct object_id }struct bulk_checkin_source { -enum {SOURCE_FILE } type; +enum {SOURCE_FILE ,SOURCE_INCORE } type;/* SOURCE_FILE fields */ int fd; +/* SOURCE_INCORE fields */ +const void *buf; +size_t read; +/* common fields */ size_t size;const char *path;
@@ -157,6 +161,11 @@ static off_t bulk_checkin_source_seek_to(struct bulk_checkin_source *source,switch (source->type) {case SOURCE_FILE :return lseek (source->fd, offset,SEEK_SET ); +case SOURCE_INCORE : +if (!(0 <= offset && offset < source->size)) +return (off_t)-1 ; + source->read = offset; +return source->read;default :BUG ("unknown bulk-checkin source: %d" , source->type); }
@@ -168,6 +177,13 @@ static ssize_t bulk_checkin_source_read(struct bulk_checkin_source *source,switch (source->type) {case SOURCE_FILE :return read_in_full (source->fd, buf, nr); +case SOURCE_INCORE : +assert (source->read <= source->size); +if (nr > source->size - source->read) + nr = source->size - source->read; +memcpy (buf, (unsigned char *)source->buf + source->read, nr); + source->read += nr; +return nr;default :BUG ("unknown bulk-checkin source: %d" , source->type); }
Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 7/10 bulk-checkin: generify `stream_blob_to_pack()` for arbitrary types
In reply to Taylor BlauThe existing stream_blob_to_pack() function is named based on the fact that it knows only how to stream blobs into a bulk-checkin pack.
But there is no longer anything in this function which prevents us from writing objects of arbitrary types to the bulk-checkin pack. Prepare to write OBJ_TREEs by removing this assumption, adding an enum object_type parameter to this function's argument list, and renaming it to stream_obj_to_pack() accordingly.
--- bulk-checkin.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-)
bulk-checkin.c +7 -8
@@ -204,10 +204,10 @@ static ssize_t bulk_checkin_source_read(struct bulk_checkin_source *source,* status before calling us just in case we ask it to call us again * with a new pack. */ -static int stream_blob_to_pack (struct bulk_checkin_packfile *state, -git_hash_ctx *ctx,off_t *already_hashed_to, -struct bulk_checkin_source *source, -unsigned flags) +static int stream_obj_to_pack (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx,off_t *already_hashed_to, +struct bulk_checkin_source *source, +enum object_type type,unsigned flags) {git_zstream s;unsigned char ibuf[16384 ];
@@ -220,8 +220,7 @@ static int stream_blob_to_pack(struct bulk_checkin_packfile *state,git_deflate_init (&s, pack_compression_level); - hdrlen =encode_in_pack_object_header (obuf,sizeof (obuf),OBJ_BLOB , - size); + hdrlen =encode_in_pack_object_header (obuf,sizeof (obuf), type, size); s.next_out = obuf + hdrlen; s.avail_out =sizeof (obuf) - hdrlen;
@@ -402,8 +401,8 @@ static int deflate_blob_to_pack(struct bulk_checkin_packfile *state,while (1 ) {prepare_checkpoint (state, &checkpoint, idx, flags); -if (!stream_blob_to_pack (state, &ctx, &already_hashed_to, - &source, flags)) +if (!stream_obj_to_pack (state, &ctx, &already_hashed_to, + &source,OBJ_BLOB , flags))break ;truncate_checkpoint (state, &checkpoint, idx);if (bulk_checkin_source_seek_to (&source, seekback) == (off_t)-1 )
Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 8/10 bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
In reply to Taylor BlauNow that we have factored out many of the common routines necessary to index a new object into a pack created by the bulk-checkin machinery, we can introduce a variant of index_blob_bulk_checkin() that acts on blobs whose contents we can fit in memory.
This will be useful in a couple of more commits in order to provide the merge-tree builtin with a mechanism to create a new pack containing any objects it created during the merge, instead of storing those objects individually as loose.
Similar to the existing index_blob_bulk_checkin() function, the entrypoint delegates to deflate_blob_to_pack_incore(), which is responsible for formatting the pack header and then deflating the contents into the pack. The latter is accomplished by calling deflate_obj_contents_to_pack_incore(), which takes advantage of the earlier refactorings and is responsible for writing the object to the pack and handling any overage from pack.packSizeLimit.
The bulk of the new functionality is implemented in the function stream_obj_to_pack(), which can handle streaming objects from memory to the bulk-checkin pack as a result of the earlier refactoring.
Consistent with the rest of the bulk-checkin mechanism, there are no direct tests here. In future commits when we expose this new functionality via the merge-tree builtin, we will test it indirectly there.
--- bulk-checkin.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ bulk-checkin.h | 4 ++++ 2 files changed, 68 insertions(+)
bulk-checkin.c +64 -0
@@ -370,6 +370,59 @@ static void finalize_checkpoint(struct bulk_checkin_packfile *state, } } +static int deflate_obj_contents_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +struct object_id *result_oid, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +struct pack_idx_entry *idx =NULL ; +off_t already_hashed_to =0 ; +struct bulk_checkin_source source = { + .type =SOURCE_INCORE , + .buf = buf, + .size = size, + .read =0 , + .path = path, + }; + +/* Note: idx is non-NULL when we are writing */ +if (flags &HASH_WRITE_OBJECT ) +CALLOC_ARRAY (idx,1 ); + +while (1 ) { +prepare_checkpoint (state, checkpoint, idx, flags); + +if (!stream_obj_to_pack (state, ctx, &already_hashed_to, &source, + type, flags)) +break ; +truncate_checkpoint (state, checkpoint, idx); +bulk_checkin_source_seek_to (&source,0 ); + } + +finalize_checkpoint (state, ctx, checkpoint, idx, result_oid); + +return 0 ; +} + +static int deflate_blob_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; +struct hashfile_checkpoint checkpoint = {0 }; + +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_BLOB , + size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, &checkpoint, + result_oid, buf, size, +OBJ_BLOB , path, flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -456,6 +509,17 @@ int index_blob_bulk_checkin(struct object_id *oid,return status; } +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_blob_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;
bulk-checkin.h +4 -0
@@ -13,6 +13,10 @@ int index_blob_bulk_checkin(struct object_id *oid, int fd, size_t size, const char *path, unsigned flags); +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Signature · 1 line
2.42.0.408.g97fac66ae4
Re: [PATCH v3 08/10] bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
Quoted 4 lines · Taylor Blau
Now that we have factored out many of the common routines necessary to index a new object into a pack created by the bulk-checkin machinery, we can introduce a variant of
index_blob_bulk_checkin()that acts on blobs whose contents we can fit in memory.
Hmph.
Doesn't the duplication between the main loop of the new deflate_obj_contents_to_pack() with existing deflate_blob_to_pack() bother you?
A similar duplication in the previous round resulted in a nice refactoring of patches 5 and 6 in this round. Compared to that, the differences in the set-up code between the two functions may be much larger, but subtle and unnecessary differences between the code that was copied and pasted (e.g., we do not check the errors from the seek_to method here, but the original does) is a sign that over time a fix to one will need to be carried over to the other, adding unnecessary maintenance burden, isn't it?
Quoted 89 lines
bulk-checkin.c +64
-0+static int deflate_obj_contents_to_pack_incore (struct bulk_checkin_packfile *state, +git_hash_ctx *ctx, +struct hashfile_checkpoint *checkpoint, +struct object_id *result_oid, +const void *buf,size_t size, +enum object_type type, +const char *path,unsigned flags) +{ +struct pack_idx_entry *idx =NULL ; +off_t already_hashed_to =0 ; +struct bulk_checkin_source source = { + .type =SOURCE_INCORE , + .buf = buf, + .size = size, + .read =0 , + .path = path, + }; + +/* Note: idx is non-NULL when we are writing */ +if (flags &HASH_WRITE_OBJECT ) +CALLOC_ARRAY (idx,1 ); + +while (1 ) { +prepare_checkpoint (state, checkpoint, idx, flags); + +if (!stream_obj_to_pack (state, ctx, &already_hashed_to, &source, + type, flags)) +break ; +truncate_checkpoint (state, checkpoint, idx); +bulk_checkin_source_seek_to (&source,0 ); + } + +finalize_checkpoint (state, ctx, checkpoint, idx, result_oid); + +return 0 ; +} + +static int deflate_blob_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; +struct hashfile_checkpoint checkpoint = {0 }; + +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_BLOB , + size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, &checkpoint, + result_oid, buf, size, +OBJ_BLOB , path, flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,@@ -456,6 +509,17 @@ int index_blob_bulk_checkin(struct object_id *oid,return status; } +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_blob_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;bulk-checkin.h +4
-0@@ -13,6 +13,10 @@ int index_blob_bulk_checkin(struct object_id *oid, int fd, size_t size, const char *path, unsigned flags); +int index_blob_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Re: [PATCH v3 08/10] bulk-checkin: introduce `index_blob_bulk_checkin_incore()`
Quoted 7 lines · Junio C Hamano
Quoted 4 lines · Taylor Blau
Now that we have factored out many of the common routines necessary to index a new object into a pack created by the bulk-checkin machinery, we can introduce a variant of
index_blob_bulk_checkin()that acts on blobs whose contents we can fit in memory.
Quoted 5 lines
Hmph.
Doesn't the duplication between the main loop of the new
deflate_obj_contents_to_pack()with existingdeflate_blob_to_pack()bother you?
Yeah, I am not sure how I missed seeing the opportunity to clean that up. Another (hopefully final...) reroll incoming.
Thanks,
Taylor
PATCH v3 9/10 bulk-checkin: introduce `index_tree_bulk_checkin_incore()`
In reply to Taylor BlauThe remaining missing piece in order to teach the merge-tree builtin how to write the contents of a merge into a pack is a function to index tree objects into a bulk-checkin pack.
This patch implements that missing piece, which is a thin wrapper around all of the functionality introduced in previous commits.
If and when Git gains support for a "compatibility" hash algorithm, the changes to support that here will be minimal. The bulk-checkin machinery will need to convert the incoming tree to compute its length under the compatibility hash, necessary to reconstruct its header. With that information (and the converted contents of the tree), the bulk-checkin machinery will have enough to keep track of the converted object's hash in order to update the compatibility mapping.
Within deflate_tree_to_pack_incore(), the changes should be limited to something like:
struct strbuf converted = STRBUF_INIT;
if (the_repository->compat_hash_algo) {
if (convert_object_file(&compat_obj,
the_repository->hash_algo,
the_repository->compat_hash_algo, ...) < 0)
die(...);
format_object_header_hash(the_repository->compat_hash_algo,
OBJ_TREE, size);
}
/* compute the converted tree's hash using the compat algorithm */
strbuf_release(&converted);, assuming related changes throughout the rest of the bulk-checkin machinery necessary to update the hash of the converted object, which are likewise minimal in size.
--- bulk-checkin.c | 27 +++++++++++++++++++++++++++ bulk-checkin.h | 4 ++++ 2 files changed, 31 insertions(+)
bulk-checkin.c +27 -0
@@ -423,6 +423,22 @@ static int deflate_blob_to_pack_incore(struct bulk_checkin_packfile *state, OBJ_BLOB, path, flags); } +static int deflate_tree_to_pack_incore (struct bulk_checkin_packfile *state, +struct object_id *result_oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +git_hash_ctx ctx; +struct hashfile_checkpoint checkpoint = {0 }; + +format_object_header_hash (the_hash_algo, &ctx, &checkpoint,OBJ_TREE , + size); + +return deflate_obj_contents_to_pack_incore (state, &ctx, &checkpoint, + result_oid, buf, size, +OBJ_TREE , path, flags); +} + static int deflate_blob_to_pack(struct bulk_checkin_packfile *state, struct object_id *result_oid, int fd, size_t size,
@@ -520,6 +536,17 @@ int index_blob_bulk_checkin_incore(struct object_id *oid,return status; } +int index_tree_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags) +{ +int status =deflate_tree_to_pack_incore (&bulk_checkin_packfile, oid, + buf, size, path, flags); +if (!odb_transaction_nesting) +flush_bulk_checkin_packfile (&bulk_checkin_packfile); +return status; +} +void begin_odb_transaction (void ) { odb_transaction_nesting +=1 ;
bulk-checkin.h +4 -0
@@ -17,6 +17,10 @@ int index_blob_bulk_checkin_incore(struct object_id *oid, const void *buf, size_t size, const char *path, unsigned flags); +int index_tree_bulk_checkin_incore (struct object_id *oid, +const void *buf,size_t size, +const char *path,unsigned flags); +/* * Tell the object database to optimize for adding * multiple objects. end_odb_transaction must be called
Signature · 1 line
2.42.0.408.g97fac66ae4
PATCH v3 10/10 builtin/merge-tree.c: implement support for `--write-pack`
In reply to Taylor BlauWhen using merge-tree often within a repository[^1], it is possible to generate a relatively large number of loose objects, which can result in degraded performance, and inode exhaustion in extreme cases.
Building on the functionality introduced in previous commits, the bulk-checkin machinery now has support to write arbitrary blob and tree objects which are small enough to be held in-core. We can use this to write any blob/tree objects generated by ORT into a separate pack instead of writing them out individually as loose.
This functionality is gated behind a new --write-pack option to merge-tree that works with the (non-deprecated) --write-tree mode.
The implementation is relatively straightforward. There are two spots within the ORT mechanism where we call write_object_file(), one for content differences within blobs, and another to assemble any new trees necessary to construct the merge. In each of those locations, conditionally replace calls to write_object_file() with index_blob_bulk_checkin_incore() or index_tree_bulk_checkin_incore() depending on which kind of object we are writing.
The only remaining task is to begin and end the transaction necessary to initialize the bulk-checkin machinery, and move any new pack(s) it created into the main object store.
[^1]: Such is the case at GitHub, where we run presumptive "test merges"
on open pull requests to see whether or not we can light up the merge button green depending on whether or not the presumptive merge was conflicted.
This is done in response to a number of user-initiated events, including viewing an open pull request whose last test merge is stale with respect to the current base and tip of the pull request. As a result, merge-tree can be run very frequently on large, active repositories.
--- Documentation/git-merge-tree.txt | 4 ++ builtin/merge-tree.c | 5 ++ merge-ort.c | 42 +++++++++++---- merge-recursive.h | 1 + t/t4301-merge-tree-write-tree.sh | 93 ++++++++++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 9 deletions(-)
Documentation/git-merge-tree.txt +4 -0
@@ -69,6 +69,10 @@ OPTIONS specify a merge-base for the merge, and specifying multiple bases is currently not supported. This option is incompatible with `--stdin`. +--write-pack:: + Write any new objects into a separate packfile instead of as + individual loose objects. + [[OUTPUT]] OUTPUT ------
builtin/merge-tree.c +5 -0
@@ -18,6 +18,7 @@#include "quote.h" #include "tree.h" #include "config.h" +#include "bulk-checkin.h" static int line_termination ='\n' ;
@@ -414,6 +415,7 @@ struct merge_tree_options {int show_messages;int name_only;int use_stdin; +int write_pack; }; static int real_merge(struct merge_tree_options *o,
@@ -440,6 +442,7 @@ static int real_merge(struct merge_tree_options *o,init_merge_options (&opt, the_repository); opt.show_rename_progress =0 ; + opt.write_pack = o->write_pack; opt.branch1 = branch1; opt.branch2 = branch2;
@@ -548,6 +551,8 @@ int cmd_merge_tree(int argc, const char **argv, const char *prefix) &merge_base, N_("commit"), N_("specify a merge-base for the merge")), + OPT_BOOL(0, "write-pack", &o.write_pack, + N_("write new objects to a pack instead of as loose")), OPT_END() };
merge-ort.c +33 -9
87 lines not on this page · raw message
merge-recursive.h +1 -0
@@ -48,6 +48,7 @@ struct merge_options { unsigned renormalize : 1; unsigned record_conflict_msgs_as_headers : 1;const char *msg_header_prefix; + unsigned write_pack : 1;/* internal fields used by the implementation */ struct merge_options_internal *priv;
t/t4301-merge-tree-write-tree.sh +93 -0
97 lines not on this page · raw message
Signature · 1 line
2.42.0.408.g97fac66ae4
v4 cover bloom: changed-path Bloom filters v2 (& sundries)
In reply to Taylor Blau(Rebased onto the tip of 'master', which is 3a06386e31 (The fifteenth batch, 2023-10-04), at the time of writing).
This series is a reroll of the combined efforts of [1] and [2] to introduce the v2 changed-path Bloom filters, which fixes a bug in our existing implementation of murmur3 paths with non-ASCII characters (when the "char" type is signed).
In large part, this is the same as the previous round. But this round includes some extra bits that address issues pointed out by SZEDER Gábor, which are:
- not reading Bloom filters for root commits
- corrupting Bloom filter reads by tweaking the filter settings between layers.
These issues were discussed in (among other places) [3], and [4], respectively.
Thanks to Jonathan, Peff, and SZEDER who have helped a great deal in assembling these patches. As usual, a range-diff is included below. Thanks in advance for your
review!
[1]: https://lore.kernel.org/git/[email protected]/
[2]: https://lore.kernel.org/git/[email protected]/
[3]: https://public-inbox.org/git/[email protected]/
[4]: https://lore.kernel.org/git/[email protected]/
Jonathan Tan (4):
gitformat-commit-graph: describe version 2 of BDAT
t4216: test changed path filters with high bit paths
repo-settings: introduce commitgraph.changedPathsVersion
commit-graph: new filter ver. that fixes murmur3
Taylor Blau (13):
t/t4216-log-bloom.sh: harden test_bloom_filters_not_used() revision.c: consult Bloom filters for root commits
commit-graph: ensure Bloom filters are read with consistent settings t/helper/test-read-graph.c: extract dump_graph_info() bloom.h: make load_bloom_filter_from_graph() public t/helper/test-read-graph: implement bloom-filters mode
bloom: annotate filters with hash version
bloom: prepare to discard incompatible Bloom filters commit-graph.c: unconditionally load Bloom filters
commit-graph: drop unnecessary graph_read_bloom_data_context object.h: fix mis-aligned flag bits table
commit-graph: reuse existing Bloom filters where possible
bloom: introduce deinit_bloom_filters()
Documentation/config/commitgraph.txt | 26 ++- Documentation/gitformat-commit-graph.txt | 9 +- bloom.c | 208 ++++++++++++++++- bloom.h | 38 ++- commit-graph.c | 61 ++++- object.h | 3 +- oss-fuzz/fuzz-commit-graph.c | 2 +- repo-settings.c | 6 +- repository.h | 2 +- revision.c | 26 ++- t/helper/test-bloom.c | 9 +- t/helper/test-read-graph.c | 67 ++++-- t/t0095-bloom.sh | 8 + t/t4216-log-bloom.sh | 282 ++++++++++++++++++++++- 14 files changed, 692 insertions(+), 55 deletions(-)
Range-diff against v3:
1: fe671d616c = 1: e0fc51c3fb t/t4216-log-bloom.sh: harden `test_bloom_filters_not_used()`
2: 7d0fa93543 = 2: 87b09e6266 revision.c: consult Bloom filters for root commits
3: 2ecc0a2d58 ! 3: 46d8a41005 commit-graph: ensure Bloom filters are read with consistent settings
@@ t/t4216-log-bloom.sh: test_expect_success 'Bloom generation backfills empty comm
+ done
+'
+
-+test_expect_success 'split' '
++test_expect_success 'ensure incompatible Bloom filters are ignored' '
+ # Compute Bloom filters with "unusual" settings.
+ git -C $repo rev-parse one >in &&
+ GIT_TEST_BLOOM_SETTINGS_NUM_HASHES=3 git -C $repo commit-graph write \
@@ t/t4216-log-bloom.sh: test_expect_success 'Bloom generation backfills empty comm
+
+test_expect_success 'merge graph layers with incompatible Bloom settings' '
+ # Ensure that incompatible Bloom filters are ignored when
-+ # generating new layers.
++ # merging existing layers.
+ git -C $repo commit-graph write --reachable --changed-paths 2>err &&
+ grep "disabling Bloom filters for commit-graph layer .$layer." err &&
+
+ test_path_is_file $repo/$graph &&
+ test_dir_is_empty $repo/$graphdir &&
+
-+ # ...and merging existing ones.
-+ git -C $repo -c core.commitGraph=false log --oneline --no-decorate -- file \
-+ >expect 2>err &&
-+ GIT_TRACE2_PERF="$(pwd)/trace.perf" \
++ git -C $repo -c core.commitGraph=false log --oneline --no-decorate -- \
++ file >expect &&
++ trace_out="$(pwd)/trace.perf" &&
++ GIT_TRACE2_PERF="$trace_out" \
+ git -C $repo log --oneline --no-decorate -- file >actual 2>err &&
+
-+ test_cmp expect actual && cat err &&
-+ grep "statistics:{\"filter_not_present\":0" trace.perf &&
-+ ! grep "disabling Bloom filters" err
++ test_cmp expect actual &&
++ grep "statistics:{\"filter_not_present\":0," trace.perf &&
++ test_must_be_empty err
+'
+
test_done
4: 17703ed89a = 4: 4d0190a992 gitformat-commit-graph: describe version 2 of BDAT
5: 94552abf45 = 5: 3c2057c11c t/helper/test-read-graph.c: extract `dump_graph_info()`
6: 3d81efa27b = 6: e002e35004 bloom.h: make `load_bloom_filter_from_graph()` public
7: d23cd89037 = 7: c7016f51cd t/helper/test-read-graph: implement `bloom-filters` mode
8: cba766f224 ! 8: cef2aac8ba t4216: test changed path filters with high bit paths
@@ Commit message
## t/t4216-log-bloom.sh ##
@@ t/t4216-log-bloom.sh: test_expect_success 'merge graph layers with incompatible Bloom settings' '
- ! grep "disabling Bloom filters" err
+ test_must_be_empty err
'
+get_first_changed_path_filter () {
@@ t/t4216-log-bloom.sh: test_expect_success 'merge graph layers with incompatible
+ (
+ cd highbit1 &&
+ echo "52a9" >expect &&
-+ get_first_changed_path_filter >actual &&
-+ test_cmp expect actual
++ get_first_changed_path_filter >actual
+ )
+'
+
9: a08a961f41 = 9: 36d4e2202e repo-settings: introduce commitgraph.changedPathsVersion
10: 61d44519a5 ! 10: f6ab427ead commit-graph: new filter ver. that fixes murmur3
@@ t/t4216-log-bloom.sh: test_expect_success 'version 1 changed-path used when vers
+ test_commit -C doublewrite c "$CENT" &&
+ git -C doublewrite config --add commitgraph.changedPathsVersion 1 &&
+ git -C doublewrite commit-graph write --reachable --changed-paths &&
++ for v in -2 3
++ do
++ git -C doublewrite config --add commitgraph.changedPathsVersion $v &&
++ git -C doublewrite commit-graph write --reachable --changed-paths 2>err &&
++ cat >expect <<-EOF &&
++ warning: attempting to write a commit-graph, but ${SQ}commitgraph.changedPathsVersion${SQ} ($v) is not supported
++ EOF
++ test_cmp expect err || return 1
++ done &&
+ git -C doublewrite config --add commitgraph.changedPathsVersion 2 &&
+ git -C doublewrite commit-graph write --reachable --changed-paths &&
+ (
11: a8c10f8de8 = 11: dc69b28329 bloom: annotate filters with hash version
12: 2ba10a4b4b = 12: 85dbdc4ed2 bloom: prepare to discard incompatible Bloom filters
13: 09d8669c3a = 13: 3ff669a622 commit-graph.c: unconditionally load Bloom filters
14: 0d4f9dc4ee = 14: 1c78e3d178 commit-graph: drop unnecessary `graph_read_bloom_data_context`
15: 1f7f27bc47 = 15: a289514faa object.h: fix mis-aligned flag bits table
16: abbef95ae8 ! 16: 6a12e39e7f commit-graph: reuse existing Bloom filters where possible
@@ t/t4216-log-bloom.sh: test_expect_success 'when writing another commit graph, pr
test_commit -C doublewrite c "$CENT" &&
+
git -C doublewrite config --add commitgraph.changedPathsVersion 1 &&
-- git -C doublewrite commit-graph write --reachable --changed-paths &&
+ GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \
+ git -C doublewrite commit-graph write --reachable --changed-paths &&
+ test_filter_computed 1 trace2.txt &&
+ test_filter_upgraded 0 trace2.txt &&
++
+ git -C doublewrite commit-graph write --reachable --changed-paths &&
+ for v in -2 3
+ do
+@@ t/t4216-log-bloom.sh: test_expect_success 'when writing commit graph, do not reuse changed-path of ano
+ EOF
+ test_cmp expect err || return 1
+ done &&
+
git -C doublewrite config --add commitgraph.changedPathsVersion 2 &&
- git -C doublewrite commit-graph write --reachable --changed-paths &&17: ca362408d5 ! 17: 8942f205c8 bloom: introduce deinit_bloom_filters()
@@ bloom.h: void add_key_to_filter(const struct bloom_key *key,
BLOOM_NOT_COMPUTED = (1 << 0),
## commit-graph.c ##
-@@ commit-graph.c: static void close_commit_graph_one(struct commit_graph *g) +@@ commit-graph.c: struct bloom_filter_settings *get_bloom_filter_settings(struct repository *r)
void close_commit_graph(struct raw_object_store *o)
{
- close_commit_graph_one(o->commit_graph);
+ clear_commit_graph_data_slab(&commit_graph_data_slab);
+ deinit_bloom_filters();
+ free_commit_graph(o->commit_graph);
o->commit_graph = NULL;
}
-
@@ commit-graph.c: int write_commit_graph(struct object_directory *odb,
res = write_commit_graph_file(ctx);
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 1/17 t/t4216-log-bloom.sh: harden `test_bloom_filters_not_used()`
The existing implementation of test_bloom_filters_not_used() asserts that the Bloom filter sub-system has not been initialized at all, by checking for the absence of any data from it from trace2.
In the following commit, it will become possible to load Bloom filters without using them (e.g., because commitGraph.changedPathVersion is incompatible with the hash version with which the commit-graph's Bloom filters were written).
When this is the case, it's possible to initialize the Bloom filter sub-system, while still not using any Bloom filters. When this is the case, check that the data dump from the Bloom sub-system is all zeros, indicating that no filters were used.
--- t/t4216-log-bloom.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-)
t/t4216-log-bloom.sh +13 -1
@@ -81,7 +81,19 @@ test_bloom_filters_used () { test_bloom_filters_not_used () { log_args=$1 setup "$log_args" && - ! grep -q "statistics:{\"filter_not_present\":" "$TRASH_DIRECTORY/trace.perf" && + + if grep -q "statistics:{\"filter_not_present\":" "$TRASH_DIRECTORY/trace.perf" + then + # if the Bloom filter system is initialized, ensure that no + # filters were used + data="statistics:{" + data="$data\"filter_not_present\":0," + data="$data\"maybe\":0," + data="$data\"definitely_not\":0," + data="$data\"false_positive\":0}" + + grep -q "$data" "$TRASH_DIRECTORY/trace.perf" + fi && test_cmp log_wo_bloom log_w_bloom }
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 2/17 revision.c: consult Bloom filters for root commits
In reply to Taylor BlauThe commit-graph stores changed-path Bloom filters which represent the set of paths included in a tree-level diff between a commit's root tree and that of its parent.
When a commit has no parents, the tree-diff is computed against that commit's root tree and the empty tree. In other words, every path in that commit's tree is stored in the Bloom filter (since they all appear in the diff).
Consult these filters during pathspec-limited traversals in the function rev_same_tree_as_empty(). Doing so yields a performance improvement where we can avoid enumerating the full set of paths in a parentless commit's root tree when we know that the path(s) of interest were not listed in that commit's changed-path Bloom filter.
--- revision.c | 26 ++++++++++++++++++++++---- t/t4216-log-bloom.sh | 8 ++++++-- 2 files changed, 28 insertions(+), 6 deletions(-)
revision.c +22 -4
@@ -834,17 +834,28 @@ static int rev_compare_tree(struct rev_info *revs,return tree_difference; } -static int rev_same_tree_as_empty (struct rev_info *revs,struct commit *commit) +static int rev_same_tree_as_empty (struct rev_info *revs,struct commit *commit, +int nth_parent) {struct tree *t1 =repo_get_commit_tree (the_repository, commit); +int bloom_ret =1 ;if (!t1)return 0 ; +if (nth_parent ==1 && revs->bloom_keys_nr) { + bloom_ret =check_maybe_different_in_bloom_filter (revs, commit); +if (!bloom_ret) +return 1 ; + } + tree_difference =REV_TREE_SAME ; revs->pruning.flags.has_changes =0 ;diff_tree_oid (NULL , &t1->object.oid,"" , &revs->pruning); +if (bloom_ret ==1 && tree_difference ==REV_TREE_SAME ) + count_bloom_filter_false_positive++; +return tree_difference ==REV_TREE_SAME ; }
@@ -882,7 +893,7 @@ static int compact_treesame(struct rev_info *revs, struct commit *commit, unsignif (nth_parent !=0 )die ("compact_treesame %u" , nth_parent); old_same = !!(commit->object.flags &TREESAME ); -if (rev_same_tree_as_empty (revs, commit)) +if (rev_same_tree_as_empty (revs, commit, nth_parent)) commit->object.flags |=TREESAME ;else commit->object.flags &= ~TREESAME ;
@@ -978,7 +989,14 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit)return ;if (!commit->parents) { -if (rev_same_tree_as_empty (revs, commit)) +/* +* Pretend as if we are comparing ourselves to the +* (non-existent) first parent of this commit object. Even +* though no such parent exists, its changed-path Bloom filter +* (if one exists) is relative to the empty tree, using Bloom +* filters is allowed here. +*/ +if (rev_same_tree_as_empty (revs, commit,1 )) commit->object.flags |=TREESAME ;return ; }
@@ -1059,7 +1077,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit)case REV_TREE_NEW :if (revs->remove_empty_trees && -rev_same_tree_as_empty (revs, p)) { +rev_same_tree_as_empty (revs, p, nth_parent)) {/* We are adding all the specified * paths from this parent, so the * history beyond this parent is not
t/t4216-log-bloom.sh +6 -2
@@ -87,7 +87,11 @@ test_bloom_filters_not_used () { # if the Bloom filter system is initialized, ensure that no # filters were used data="statistics:{" - data="$data\"filter_not_present\":0," + # unusable filters (e.g., those computed with a + # different value of commitGraph.changedPathsVersion) + # are counted in the filter_not_present bucket, so any + # value is OK there. + data="$data\"filter_not_present\":[0-9][0-9]*," data="$data\"maybe\":0," data="$data\"definitely_not\":0," data="$data\"false_positive\":0}"
@@ -174,7 +178,7 @@ test_expect_success 'setup - add commit-graph to the chain with Bloom filters' ' test_bloom_filters_used_when_some_filters_are_missing () { log_args=$1 - bloom_trace_prefix="statistics:{\"filter_not_present\":3,\"maybe\":6,\"definitely_not\":9" + bloom_trace_prefix="statistics:{\"filter_not_present\":3,\"maybe\":6,\"definitely_not\":10" setup "$log_args" && grep -q "$bloom_trace_prefix" "$TRASH_DIRECTORY/trace.perf" && test_cmp log_wo_bloom log_w_bloom
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 3/17 commit-graph: ensure Bloom filters are read with consistent settings
In reply to Taylor BlauThe changed-path Bloom filter mechanism is parameterized by a couple of variables, notably the number of bits per hash (typically "m" in Bloom filter literature) and the number of hashes themselves (typically "k").
It is critically important that filters are read with the Bloom filter settings that they were written with. Failing to do so would mean that each query is liable to compute different fingerprints, meaning that the filter itself could return a false negative. This goes against a basic assumption of using Bloom filters (that they may return false positives, but never false negatives) and can lead to incorrect results.
We have some existing logic to carry forward existing Bloom filter settings from one layer to the next. In write_commit_graph(), we have something like:
if (!(flags & COMMIT_GRAPH_NO_WRITE_BLOOM_FILTERS)) {
struct commit_graph *g = ctx->r->objects->commit_graph;
/* We have changed-paths already. Keep them in the next graph */
if (g && g->chunk_bloom_data) {
ctx->changed_paths = 1;
ctx->bloom_settings = g->bloom_filter_settings;
}
}, which drags forward Bloom filter settings across adjacent layers.
This doesn't quite address all cases, however, since it is possible for intermediate layers to contain no Bloom filters at all. For example, suppose we have two layers in a commit-graph chain, say, {G1, G2}. If G1 contains Bloom filters, but G2 doesn't, a new G3 (whose base graph is G2) may be written with arbitrary Bloom filter settings, because we only check the immediately adjacent layer's settings for compatibility.
This behavior has existed since the introduction of changed-path Bloom filters. But in practice, this is not such a big deal, since the only way up until this point to modify the Bloom filter settings at write time is with the undocumented environment variables:
- GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY
- GIT_TEST_BLOOM_SETTINGS_NUM_HASHES
- GIT_TEST_BLOOM_SETTINGS_MAX_CHANGED_PATHS
(it is still possible to tweak MAX_CHANGED_PATHS between layers, but this does not affect reads, so is allowed to differ across multiple graph layers).
But in future commits, we will introduce another parameter to change the hash algorithm used to compute Bloom fingerprints itself. This will be exposed via a configuration setting, making this foot-gun easier to use.
To prevent this potential issue, validate that all layers of a split commit-graph have compatible settings with the newest layer which contains Bloom filters.
--- commit-graph.c | 25 +++++++++++++++++ t/t4216-log-bloom.sh | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+)
commit-graph.c +25 -0
@@ -498,6 +498,30 @@ static int validate_mixed_generation_chain(struct commit_graph *g)return 0 ; } +static void validate_mixed_bloom_settings (struct commit_graph *g) +{ +struct bloom_filter_settings *settings =NULL ; +for (; g; g = g->base_graph) { +if (!g->bloom_filter_settings) +continue ; +if (!settings) { + settings = g->bloom_filter_settings; +continue ; + } + +if (g->bloom_filter_settings->bits_per_entry != settings->bits_per_entry || + g->bloom_filter_settings->num_hashes != settings->num_hashes) { + g->chunk_bloom_indexes =NULL ; + g->chunk_bloom_data =NULL ; +FREE_AND_NULL (g->bloom_filter_settings); + +warning (_ ("disabling Bloom filters for commit-graph " +"layer '%s' due to incompatible settings" ), +oid_to_hex (&g->oid)); + } + } +} + static int add_graph_to_chain(struct commit_graph *g, struct commit_graph *chain, struct object_id *oids,
@@ -616,6 +640,7 @@ struct commit_graph *load_commit_graph_chain_fd_st(struct repository *r, }validate_mixed_generation_chain (graph_chain); +validate_mixed_bloom_settings (graph_chain);free (oids);fclose (fp);
t/t4216-log-bloom.sh +64 -0
68 lines not on this page · raw message
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 4/17 gitformat-commit-graph: describe version 2 of BDAT
In reply to Taylor BlauFrom: Jonathan Tan <[email protected]>
The code change to Git to support version 2 will be done in subsequent commits.
--- Documentation/gitformat-commit-graph.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-)
Documentation/gitformat-commit-graph.txt +6 -3
@@ -142,13 +142,16 @@ All multi-byte numbers are in network byte order. ==== Bloom Filter Data (ID: {'B', 'D', 'A', 'T'}) [Optional] * It starts with header consisting of three unsigned 32-bit integers: - - Version of the hash algorithm being used. We currently only support - value 1 which corresponds to the 32-bit version of the murmur3 hash + - Version of the hash algorithm being used. We currently support + value 2 which corresponds to the 32-bit version of the murmur3 hash implemented exactly as described in https://en.wikipedia.org/wiki/MurmurHash#Algorithm and the double hashing technique using seed values 0x293ae76f and 0x7e646e2 as described in https://doi.org/10.1007/978-3-540-30494-4_26 "Bloom Filters - in Probabilistic Verification" + in Probabilistic Verification". Version 1 Bloom filters have a bug that appears + when char is signed and the repository has path names that have characters >= + 0x80; Git supports reading and writing them, but this ability will be removed + in a future version of Git. - The number of times a path is hashed and hence the number of bit positions that cumulatively determine whether a file is present in the commit. - The minimum number of bits 'b' per entry in the Bloom filter. If the filter
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 5/17 t/helper/test-read-graph.c: extract `dump_graph_info()`
In reply to Taylor BlauPrepare for the 'read-graph' test helper to perform other tasks besides dumping high-level information about the commit-graph by extracting its main routine into a separate function.
--- t/helper/test-read-graph.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-)
t/helper/test-read-graph.c +18 -13
@@ -5,20 +5,8 @@#include "bloom.h" #include "setup.h" -int cmd__read_graph(int argc UNUSED, const char **argv UNUSED) +static void dump_graph_info (struct commit_graph *graph) { -struct commit_graph *graph =NULL ; -struct object_directory *odb; - -setup_git_directory (); - odb = the_repository->objects->odb; - -prepare_repo_settings (the_repository); - - graph =read_commit_graph_one (the_repository, odb); -if (!graph) -return 1 ; - printf("header: %08x %d %d %d %d\n", ntohl(*(uint32_t*)graph->data), *(unsigned char*)(graph->data + 4),
@@ -57,6 +45,23 @@ int cmd__read_graph(int argc UNUSED, const char **argv UNUSED)if (graph->topo_levels)printf (" topo_levels" );printf ("\n" ); +} + +int cmd__read_graph(int argc UNUSED, const char **argv UNUSED) +{ +struct commit_graph *graph =NULL ; +struct object_directory *odb; + +setup_git_directory (); + odb = the_repository->objects->odb; + +prepare_repo_settings (the_repository); + + graph =read_commit_graph_one (the_repository, odb); +if (!graph) +return 1 ; + +dump_graph_info (graph);UNLEAK (graph);
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 6/17 bloom.h: make `load_bloom_filter_from_graph()` public
In reply to Taylor BlauPrepare for a future commit to use the load_bloom_filter_from_graph() function directly to load specific Bloom filters out of the commit-graph for manual inspection (to be used during tests).
--- bloom.c | 6 +++--- bloom.h | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-)
bloom.c +3 -3
@@ -29,9 +29,9 @@ static inline unsigned char get_bitmask(uint32_t pos)return ((unsigned char )1 ) << (pos & (BITS_PER_WORD -1 )); } -static int load_bloom_filter_from_graph (struct commit_graph *g, -struct bloom_filter *filter, -uint32_t graph_pos) +int load_bloom_filter_from_graph (struct commit_graph *g, +struct bloom_filter *filter, +uint32_t graph_pos) {uint32_t lex_pos, start_index, end_index;
bloom.h +5 -0
@@ -3,6 +3,7 @@struct commit ;struct repository ; +struct commit_graph ;struct bloom_filter_settings {/*
@@ -68,6 +69,10 @@ struct bloom_key {uint32_t *hashes; }; +int load_bloom_filter_from_graph (struct commit_graph *g, +struct bloom_filter *filter, +uint32_t graph_pos); +/* * Calculate the murmur3 32-bit hash value for the given data * using the given seed.
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 7/17 t/helper/test-read-graph: implement `bloom-filters` mode
In reply to Taylor BlauImplement a mode of the "read-graph" test helper to dump out the hexadecimal contents of the Bloom filter(s) contained in a commit-graph.
--- t/helper/test-read-graph.c | 44 +++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-)
t/helper/test-read-graph.c +39 -5
@@ -47,10 +47,32 @@ static void dump_graph_info(struct commit_graph *graph)printf ("\n" ); } -int cmd__read_graph(int argc UNUSED, const char **argv UNUSED) +static void dump_graph_bloom_filters (struct commit_graph *graph) +{ +uint32_t i; + +for (i =0 ; i < graph->num_commits + graph->num_commits_in_base; i++) { +struct bloom_filter filter = {0 }; +size_t j; + +if (load_bloom_filter_from_graph (graph, &filter, i) <0 ) { +fprintf (stderr,"missing Bloom filter for graph " +"position %" PRIu32"\n" , i); +continue ; + } + +for (j =0 ; j < filter.len; j++) +printf ("%02x" , filter.data[j]); +if (filter.len) +printf ("\n" ); + } +} + +int cmd__read_graph (int argc,const char **argv) {struct commit_graph *graph =NULL ;struct object_directory *odb; +int ret =0 ;setup_git_directory (); odb = the_repository->objects->odb;
@@ -58,12 +80,24 @@ int cmd__read_graph(int argc UNUSED, const char **argv UNUSED)prepare_repo_settings (the_repository); graph =read_commit_graph_one (the_repository, odb); -if (!graph) -return 1 ; +if (!graph) { + ret =1 ; + goto done; + } -dump_graph_info (graph); +if (argc <=1 ) +dump_graph_info (graph); +else if (!strcmp (argv[1 ],"bloom-filters" )) +dump_graph_bloom_filters (graph); +else { +fprintf (stderr,"unknown sub-command: '%s'\n" , argv[1 ]); + ret =1 ; + } +done:UNLEAK (graph); -return 0 ; +return ret; } + +
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 8/17 t4216: test changed path filters with high bit paths
In reply to Taylor BlauFrom: Jonathan Tan <[email protected]>
Subsequent commits will teach Git another version of changed path filter that has different behavior with paths that contain at least one character with its high bit set, so test the existing behavior as a baseline.
--- t/t4216-log-bloom.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+)
t/t4216-log-bloom.sh +51 -0
@@ -484,4 +484,55 @@ test_expect_success 'merge graph layers with incompatible Bloom settings' ' test_must_be_empty err ' +get_first_changed_path_filter () { + test-tool read-graph bloom-filters >filters.dat && + head -n 1 filters.dat +} + +# chosen to be the same under all Unicode normalization forms +CENT=$(printf "\302\242") + +test_expect_success 'set up repo with high bit path, version 1 changed-path' ' + git init highbit1 && + test_commit -C highbit1 c1 "$CENT" && + git -C highbit1 commit-graph write --reachable --changed-paths +' + +test_expect_success 'setup check value of version 1 changed-path' ' + ( + cd highbit1 && + echo "52a9" >expect && + get_first_changed_path_filter >actual + ) +' + +# expect will not match actual if char is unsigned by default. Write the test +# in this way, so that a user running this test script can still see if the two +# files match. (It will appear as an ordinary success if they match, and a skip +# if not.) +if test_cmp highbit1/expect highbit1/actual +then + test_set_prereq SIGNED_CHAR_BY_DEFAULT +fi +test_expect_success SIGNED_CHAR_BY_DEFAULT 'check value of version 1 changed-path' ' + # Only the prereq matters for this test. + true +' + +test_expect_success 'setup make another commit' ' + # "git log" does not use Bloom filters for root commits - see how, in + # revision.c, rev_compare_tree() (the only code path that eventually calls + # get_bloom_filter()) is only called by try_to_simplify_commit() when the commit + # has one parent. Therefore, make another commit so that we perform the tests on + # a non-root commit. + test_commit -C highbit1 anotherc1 "another$CENT" +' + +test_expect_success 'version 1 changed-path used when version 1 requested' ' + ( + cd highbit1 && + test_bloom_filters_used "-- another$CENT" + ) +' + test_done
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 9/17 repo-settings: introduce commitgraph.changedPathsVersion
In reply to Taylor BlauFrom: Jonathan Tan <[email protected]>
A subsequent commit will introduce another version of the changed-path filter in the commit graph file. In order to control which version to write (and read), a config variable is needed.
Therefore, introduce this config variable. For forwards compatibility, teach Git to not read commit graphs when the config variable is set to an unsupported version. Because we teach Git this, commitgraph.readChangedPaths is now redundant, so deprecate it and define its behavior in terms of the config variable we introduce.
This commit does not change the behavior of writing (Git writes changed path filters when explicitly instructed regardless of any config variable), but a subsequent commit will restrict Git such that it will only write when commitgraph.changedPathsVersion is a recognized value.
--- Documentation/config/commitgraph.txt | 23 ++++++++++++++++++++--- commit-graph.c | 2 +- oss-fuzz/fuzz-commit-graph.c | 2 +- repo-settings.c | 6 +++++- repository.h | 2 +- 5 files changed, 28 insertions(+), 7 deletions(-)
Documentation/config/commitgraph.txt +20 -3
@@ -9,6 +9,23 @@ commitGraph.maxNewFilters:: commit-graph write` (c.f., linkgit:git-commit-graph[1]). commitGraph.readChangedPaths:: - If true, then git will use the changed-path Bloom filters in the - commit-graph file (if it exists, and they are present). Defaults to - true. See linkgit:git-commit-graph[1] for more information. + Deprecated. Equivalent to commitGraph.changedPathsVersion=-1 if true, and + commitGraph.changedPathsVersion=0 if false. (If commitGraph.changedPathVersion + is also set, commitGraph.changedPathsVersion takes precedence.) + +commitGraph.changedPathsVersion:: + Specifies the version of the changed-path Bloom filters that Git will read and + write. May be -1, 0 or 1. ++ +Defaults to -1. ++ +If -1, Git will use the version of the changed-path Bloom filters in the +repository, defaulting to 1 if there are none. ++ +If 0, Git will not read any Bloom filters, and will write version 1 Bloom +filters when instructed to write. ++ +If 1, Git will only read version 1 Bloom filters, and will write version 1 +Bloom filters. ++ +See linkgit:git-commit-graph[1] for more information.
commit-graph.c +1 -1
@@ -411,7 +411,7 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, graph->read_generation_data =1 ; } -if (s->commit_graph_read_changed_paths) { +if (s->commit_graph_changed_paths_version) {pair_chunk (cf,GRAPH_CHUNKID_BLOOMINDEXES , &graph->chunk_bloom_indexes); read_chunk(cf, GRAPH_CHUNKID_BLOOMDATA,
oss-fuzz/fuzz-commit-graph.c +1 -1
@@ -19,7 +19,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)* possible. */ the_repository->settings.commit_graph_generation_version =2 ; - the_repository->settings.commit_graph_read_changed_paths =1 ; + the_repository->settings.commit_graph_changed_paths_version =1 ; g =parse_commit_graph (&the_repository->settings, (void *)data, size);repo_clear (the_repository);free_commit_graph (g);
repo-settings.c +5 -1
@@ -24,6 +24,7 @@ void prepare_repo_settings(struct repository *r)int value;const char *strval;int manyfiles; +int read_changed_paths;if (!r->gitdir)BUG ("Cannot add settings for uninitialized repository" );
@@ -54,7 +55,10 @@ void prepare_repo_settings(struct repository *r)/* Commit graph config or default, does not cascade (simple) */ repo_cfg_bool (r,"core.commitgraph" , &r->settings.core_commit_graph,1 );repo_cfg_int (r,"commitgraph.generationversion" , &r->settings.commit_graph_generation_version,2 ); -repo_cfg_bool (r,"commitgraph.readchangedpaths" , &r->settings.commit_graph_read_changed_paths,1 ); +repo_cfg_bool (r,"commitgraph.readchangedpaths" , &read_changed_paths,1 ); +repo_cfg_int (r,"commitgraph.changedpathsversion" , + &r->settings.commit_graph_changed_paths_version, + read_changed_paths ?-1 :0 );repo_cfg_bool (r,"gc.writecommitgraph" , &r->settings.gc_write_commit_graph,1 );repo_cfg_bool (r,"fetch.writecommitgraph" , &r->settings.fetch_write_commit_graph,0 );
repository.h +1 -1
@@ -29,7 +29,7 @@ struct repo_settings {int core_commit_graph;int commit_graph_generation_version; -int commit_graph_read_changed_paths; +int commit_graph_changed_paths_version;int gc_write_commit_graph;int fetch_write_commit_graph;int command_requires_full_index;
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 10/17 commit-graph: new filter ver. that fixes murmur3
In reply to Taylor BlauFrom: Jonathan Tan <[email protected]>
The murmur3 implementation in bloom.c has a bug when converting series of 4 bytes into network-order integers when char is signed (which is controllable by a compiler option, and the default signedness of char is platform-specific). When a string contains characters with the high bit set, this bug causes results that, although internally consistent within Git, does not accord with other implementations of murmur3 (thus, the changed path filters wouldn't be readable by other off-the-shelf implementatios of murmur3) and even with Git binaries that were compiled with different signedness of char. This bug affects both how Git writes changed path filters to disk and how Git interprets changed path filters on disk.
Therefore, introduce a new version (2) of changed path filters that corrects this problem. The existing version (1) is still supported and is still the default, but users should migrate away from it as soon as possible.
Because this bug only manifests with characters that have the high bit set, it may be possible that some (or all) commits in a given repo would have the same changed path filter both before and after this fix is applied. However, in order to determine whether this is the case, the changed paths would first have to be computed, at which point it is not much more expensive to just compute a new changed path filter.
So this patch does not include any mechanism to "salvage" changed path filters from repositories. There is also no "mixed" mode - for each invocation of Git, reading and writing changed path filters are done with the same version number; this version number may be explicitly stated (typically if the user knows which version they need) or automatically determined from the version of the existing changed path filters in the repository.
There is a change in write_commit_graph(). graph_read_bloom_data() makes it possible for chunk_bloom_data to be non-NULL but bloom_filter_settings to be NULL, which causes a segfault later on. I produced such a segfault while developing this patch, but couldn't find a way to reproduce it neither after this complete patch (or before), but in any case it seemed like a good thing to include that might help future patch authors.
The value in t0095 was obtained from another murmur3 implementation using the following Go source code:
package main
import "fmt"
import "github.com/spaolacci/murmur3"
func main() {
fmt.Printf("%x\n", murmur3.Sum32([]byte("Hello world!")))
fmt.Printf("%x\n", murmur3.Sum32([]byte{0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}))
}--- Documentation/config/commitgraph.txt | 5 +- bloom.c | 69 +++++++++++++++- bloom.h | 8 +- commit-graph.c | 32 ++++++-- t/helper/test-bloom.c | 9 ++- t/t0095-bloom.sh | 8 ++ t/t4216-log-bloom.sh | 114 +++++++++++++++++++++++++++ 7 files changed, 232 insertions(+), 13 deletions(-)
Documentation/config/commitgraph.txt +4 -1
@@ -15,7 +15,7 @@ commitGraph.readChangedPaths:: commitGraph.changedPathsVersion:: Specifies the version of the changed-path Bloom filters that Git will read and - write. May be -1, 0 or 1. + write. May be -1, 0, 1, or 2. + Defaults to -1. +
@@ -28,4 +28,7 @@ filters when instructed to write. If 1, Git will only read version 1 Bloom filters, and will write version 1 Bloom filters. + +If 2, Git will only read version 2 Bloom filters, and will write version 2 +Bloom filters. ++ See linkgit:git-commit-graph[1] for more information.
bloom.c +66 -3
81 lines not on this page · raw message
bloom.h +5 -3
@@ -8,9 +8,11 @@ struct commit_graph;struct bloom_filter_settings {/* * The version of the hashing technique being used. -* We currently only support version = 1 which is +* The newest version is 2, which is * the seeded murmur3 hashing technique implemented -* in bloom.c. +* in bloom.c. Bloom filters of version 1 were created +* with prior versions of Git, which had a bug in the +* implementation of the hash function. */ uint32_t hash_version;
@@ -80,7 +82,7 @@ int load_bloom_filter_from_graph(struct commit_graph *g,* Not considered to be cryptographically secure. * Implemented as described in https://en.wikipedia.org/wiki/MurmurHash#Algorithm */ -uint32_t murmur3_seeded (uint32_t seed,const char *data,size_t len); +uint32_t murmur3_seeded_v2 (uint32_t seed,const char *data,size_t len); void fill_bloom_key(const char *data, size_t len,
commit-graph.c +27 -5
@@ -314,17 +314,26 @@ static int graph_read_oid_lookup(const unsigned char *chunk_start,return 0 ; } +struct graph_read_bloom_data_context { +struct commit_graph *g; +int *commit_graph_changed_paths_version; +}; +static int graph_read_bloom_data (const unsigned char *chunk_start,size_t chunk_size,void *data) { -struct commit_graph *g = data; +struct graph_read_bloom_data_context *c = data; +struct commit_graph *g = c->g;uint32_t hash_version; - g->chunk_bloom_data = chunk_start; hash_version =get_be32 (chunk_start); -if (hash_version !=1 ) +if (*c->commit_graph_changed_paths_version ==-1 ) { + *c->commit_graph_changed_paths_version = hash_version; + }else if (hash_version != *c->commit_graph_changed_paths_version) {return 0 ; + } + g->chunk_bloom_data = chunk_start; g->bloom_filter_settings =xmalloc (sizeof (struct bloom_filter_settings )); g->bloom_filter_settings->hash_version = hash_version; g->bloom_filter_settings->num_hashes =get_be32 (chunk_start +4 );
@@ -412,10 +421,14 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, }if (s->commit_graph_changed_paths_version) { +struct graph_read_bloom_data_context context = { + .g = graph, + .commit_graph_changed_paths_version = &s->commit_graph_changed_paths_version + };pair_chunk (cf,GRAPH_CHUNKID_BLOOMINDEXES , &graph->chunk_bloom_indexes);read_chunk (cf,GRAPH_CHUNKID_BLOOMDATA , - graph_read_bloom_data, graph); + graph_read_bloom_data, &context); }if (graph->chunk_bloom_indexes && graph->chunk_bloom_data) {
@@ -2436,6 +2449,13 @@ int write_commit_graph(struct object_directory *odb, }if (!commit_graph_compatible (r))return 0 ; +if (r->settings.commit_graph_changed_paths_version <-1 + || r->settings.commit_graph_changed_paths_version >2 ) { +warning (_ ("attempting to write a commit-graph, but " +"'commitgraph.changedPathsVersion' (%d) is not supported" ), + r->settings.commit_graph_changed_paths_version); +return 0 ; + }CALLOC_ARRAY (ctx,1 ); ctx->r = r;
@@ -2448,6 +2468,8 @@ int write_commit_graph(struct object_directory *odb, ctx->write_generation_data = (get_configured_generation_version (r) ==2 ); ctx->num_generation_data_overflows =0 ; + bloom_settings.hash_version = r->settings.commit_graph_changed_paths_version ==2 + ?2 :1 ; bloom_settings.bits_per_entry =git_env_ulong ("GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY" , bloom_settings.bits_per_entry); bloom_settings.num_hashes = git_env_ulong("GIT_TEST_BLOOM_SETTINGS_NUM_HASHES",
@@ -2477,7 +2499,7 @@ int write_commit_graph(struct object_directory *odb, g = ctx->r->objects->commit_graph;/* We have changed-paths already. Keep them in the next graph */ -if (g && g->chunk_bloom_data) { +if (g && g->bloom_filter_settings) { ctx->changed_paths =1 ; ctx->bloom_settings = g->bloom_filter_settings; }
t/helper/test-bloom.c +8 -1
@@ -50,6 +50,7 @@ static void get_bloom_filter_for_commit(const struct object_id *commit_oid)static const char *bloom_usage ="\n" " test-tool bloom get_murmur3 <string>\n" +" test-tool bloom get_murmur3_seven_highbit\n" " test-tool bloom generate_filter <string> [<string>...]\n" " test-tool bloom get_filter_for_commit <commit-hex>\n" ;
@@ -64,7 +65,13 @@ int cmd__bloom(int argc, const char **argv)uint32_t hashed;if (argc <3 )usage (bloom_usage); - hashed =murmur3_seeded (0 , argv[2 ],strlen (argv[2 ])); + hashed =murmur3_seeded_v2 (0 , argv[2 ],strlen (argv[2 ])); +printf ("Murmur3 Hash with seed=0:0x%08x\n" , hashed); + } + +if (!strcmp (argv[1 ],"get_murmur3_seven_highbit" )) { +uint32_t hashed; + hashed =murmur3_seeded_v2 (0 ,"\x99\xaa\xbb\xcc\xdd\xee\xff" ,7 );printf ("Murmur3 Hash with seed=0:0x%08x\n" , hashed); }
t/t0095-bloom.sh +8 -0
@@ -29,6 +29,14 @@ test_expect_success 'compute unseeded murmur3 hash for test string 2' ' test_cmp expect actual ' +test_expect_success 'compute unseeded murmur3 hash for test string 3' ' + cat >expect <<-\EOF && + Murmur3 Hash with seed=0:0xa183ccfd + EOF + test-tool bloom get_murmur3_seven_highbit >actual && + test_cmp expect actual +' + test_expect_success 'compute bloom key for empty string' ' cat >expect <<-\EOF && Hashes:0x5615800c|0x5b966560|0x61174ab4|0x66983008|0x6c19155c|0x7199fab0|0x771ae004|
t/t4216-log-bloom.sh +114 -0
118 lines not on this page · raw message
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 11/17 bloom: annotate filters with hash version
In reply to Taylor BlauIn subsequent commits, we will want to load existing Bloom filters out of a commit-graph, even when the hash version they were computed with does not match the value of commitGraph.changedPathVersion.
In order to differentiate between the two, add a "version" field to each Bloom filter.
--- bloom.c | 11 ++++++++--- bloom.h | 1 + 2 files changed, 9 insertions(+), 3 deletions(-)
bloom.c +8 -3
@@ -55,6 +55,7 @@ int load_bloom_filter_from_graph(struct commit_graph *g, filter->data = (unsigned char *)(g->chunk_bloom_data +sizeof (unsigned char ) * start_index +BLOOMDATA_CHUNK_HEADER_SIZE ); + filter->version = g->bloom_filter_settings->hash_version;return 1 ; }
@@ -240,11 +241,13 @@ static int pathmap_cmp(const void *hashmap_cmp_fn_data UNUSED,return strcmp (e1->path, e2->path); } -static void init_truncated_large_filter (struct bloom_filter *filter) +static void init_truncated_large_filter (struct bloom_filter *filter, +int version) { filter->data =xmalloc (1 ); filter->data[0 ] =0xFF ; filter->len =1 ; + filter->version = version; } struct bloom_filter *get_or_compute_bloom_filter(struct repository *r,
@@ -329,13 +332,15 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, }if (hashmap_get_size (&pathmap) > settings->max_changed_paths) { -init_truncated_large_filter (filter); +init_truncated_large_filter (filter, + settings->hash_version);if (computed) *computed |=BLOOM_TRUNC_LARGE ; goto cleanup; } filter->len = (hashmap_get_size (&pathmap) * settings->bits_per_entry +BITS_PER_WORD -1 ) /BITS_PER_WORD ; + filter->version = settings->hash_version;if (!filter->len) {if (computed) *computed |=BLOOM_TRUNC_EMPTY ;
@@ -355,7 +360,7 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, }else {for (i =0 ; i < diff_queued_diff.nr; i++)diff_free_filepair (diff_queued_diff.queue[i]); -init_truncated_large_filter (filter); +init_truncated_large_filter (filter, settings->hash_version);if (computed) *computed |=BLOOM_TRUNC_LARGE ;
bloom.h +1 -0
@@ -55,6 +55,7 @@ struct bloom_filter_settings {struct bloom_filter {unsigned char *data;size_t len; +int version; };/*
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 12/17 bloom: prepare to discard incompatible Bloom filters
In reply to Taylor BlauCallers use the inline get_bloom_filter() implementation as a thin wrapper around get_or_compute_bloom_filter(). The former calls the latter with a value of "0" for compute_if_not_present, making get_bloom_filter() the default read-only path for fetching an existing Bloom filter.
Callers expect the value returned from get_bloom_filter() is usable, that is that it's compatible with the configured value corresponding to commitGraph.changedPathsVersion.
This is OK, since the commit-graph machinery only initializes its BDAT chunk (thereby enabling it to service Bloom filter queries) when the Bloom filter hash_version is compatible with our settings. So any value returned by get_bloom_filter() is trivially useable.
However, subsequent commits will load the BDAT chunk even when the Bloom filters are built with incompatible hash versions. Prepare to handle this by teaching get_bloom_filter() to discard filters that are incompatible with the configured hash version.
Callers who wish to read incompatible filters (e.g., for upgrading filters from v1 to v2) may use the lower level routine, get_or_compute_bloom_filter().
--- bloom.c | 20 +++++++++++++++++++- bloom.h | 20 ++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-)
bloom.c +19 -1
@@ -250,6 +250,23 @@ static void init_truncated_large_filter(struct bloom_filter *filter, filter->version = version; } +struct bloom_filter *get_bloom_filter (struct repository *r,struct commit *c) +{ +struct bloom_filter *filter; +int hash_version; + + filter =get_or_compute_bloom_filter (r, c,0 ,NULL ,NULL ); +if (!filter) +return NULL ; + +prepare_repo_settings (r); + hash_version = r->settings.commit_graph_changed_paths_version; + +if (!(hash_version ==-1 || hash_version == filter->version)) +return NULL ;/* unusable filter */ +return filter; +} + struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, struct commit *c, int compute_if_not_present,
@@ -275,7 +292,8 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, filter, graph_pos); } -if (filter->data && filter->len) +if ((filter->data && filter->len) && + (!settings || settings->hash_version == filter->version))return filter;if (!compute_if_not_present)return NULL ;
bloom.h +18 -2
@@ -110,8 +110,24 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r,const struct bloom_filter_settings *settings, enum bloom_filter_computed *computed); -#define get_bloom_filter (r, c) get_or_compute_bloom_filter( \ - (r), (c), 0, NULL, NULL) +/* +* Find the Bloom filter associated with the given commit "c". +* +* If any of the following are true +* +* - the repository does not have a commit-graph, or +* - the repository disables reading from the commit-graph, or +* - the given commit does not have a Bloom filter computed, or +* - there is a Bloom filter for commit "c", but it cannot be read +* because the filter uses an incompatible version of murmur3 +* +* , then `get_bloom_filter()` will return NULL. Otherwise, the corresponding +* Bloom filter will be returned. +* +* For callers who wish to inspect Bloom filters with incompatible hash +* versions, use get_or_compute_bloom_filter(). +*/ +struct bloom_filter *get_bloom_filter (struct repository *r,struct commit *c); int bloom_filter_contains(const struct bloom_filter *filter, const struct bloom_key *key,
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 13/17 commit-graph.c: unconditionally load Bloom filters
In reply to Taylor BlauIn 9e4df4da07 (commit-graph: new filter ver. that fixes murmur3, 2023-08-01), we began ignoring the Bloom data ("BDAT") chunk for commit-graphs whose Bloom filters were computed using a hash version incompatible with the value of commitGraph.changedPathVersion.
Now that the Bloom API has been hardened to discard these incompatible filters (with the exception of low-level APIs), we can safely load these Bloom filters unconditionally.
We no longer want to return early from graph_read_bloom_data(), and similarly do not want to set the bloom_settings' hash_version field as a side-effect. The latter is because we want to wait until we know which Bloom settings we're using (either the defaults, from the GIT_TEST variables, or from the previous commit-graph layer) before deciding what hash_version to use.
If we detect an existing BDAT chunk, we'll infer the rest of the settings (e.g., number of hashes, bits per entry, and maximum number of changed paths) from the earlier graph layer. The hash_version will be inferred from the previous layer as well, unless one has already been specified via configuration.
Once all of that is done, we normalize the value of the hash_version to either "1" or "2".
--- commit-graph.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-)
commit-graph.c +10 -9
@@ -327,12 +327,6 @@ static int graph_read_bloom_data(const unsigned char *chunk_start,uint32_t hash_version; hash_version =get_be32 (chunk_start); -if (*c->commit_graph_changed_paths_version ==-1 ) { - *c->commit_graph_changed_paths_version = hash_version; - }else if (hash_version != *c->commit_graph_changed_paths_version) { -return 0 ; - } - g->chunk_bloom_data = chunk_start; g->bloom_filter_settings =xmalloc (sizeof (struct bloom_filter_settings )); g->bloom_filter_settings->hash_version = hash_version;
@@ -2468,8 +2462,7 @@ int write_commit_graph(struct object_directory *odb, ctx->write_generation_data = (get_configured_generation_version (r) ==2 ); ctx->num_generation_data_overflows =0 ; - bloom_settings.hash_version = r->settings.commit_graph_changed_paths_version ==2 - ?2 :1 ; + bloom_settings.hash_version = r->settings.commit_graph_changed_paths_version; bloom_settings.bits_per_entry =git_env_ulong ("GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY" , bloom_settings.bits_per_entry); bloom_settings.num_hashes = git_env_ulong("GIT_TEST_BLOOM_SETTINGS_NUM_HASHES",
@@ -2501,10 +2494,18 @@ int write_commit_graph(struct object_directory *odb,/* We have changed-paths already. Keep them in the next graph */ if (g && g->bloom_filter_settings) { ctx->changed_paths =1 ; - ctx->bloom_settings = g->bloom_filter_settings; + +/* don't propagate the hash_version unless unspecified */ +if (bloom_settings.hash_version ==-1 ) + bloom_settings.hash_version = g->bloom_filter_settings->hash_version; + bloom_settings.bits_per_entry = g->bloom_filter_settings->bits_per_entry; + bloom_settings.num_hashes = g->bloom_filter_settings->num_hashes; + bloom_settings.max_changed_paths = g->bloom_filter_settings->max_changed_paths; } } + bloom_settings.hash_version = bloom_settings.hash_version ==2 ?2 :1 ; +if (ctx->split) {struct commit_graph *g = ctx->r->objects->commit_graph;
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 14/17 commit-graph: drop unnecessary `graph_read_bloom_data_context`
In reply to Taylor BlauThe graph_read_bloom_data_context struct was introduced in an earlier commit in order to pass pointers to the commit-graph and changed-path Bloom filter version when reading the BDAT chunk.
The previous commit no longer writes through the changed_paths_version pointer, making the surrounding context structure unnecessary. Drop it and pass a pointer to the commit-graph directly when reading the BDAT chunk.
--- commit-graph.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-)
commit-graph.c +2 -12
@@ -314,16 +314,10 @@ static int graph_read_oid_lookup(const unsigned char *chunk_start,return 0 ; } -struct graph_read_bloom_data_context { -struct commit_graph *g; -int *commit_graph_changed_paths_version; -}; -static int graph_read_bloom_data (const unsigned char *chunk_start,size_t chunk_size,void *data) { -struct graph_read_bloom_data_context *c = data; -struct commit_graph *g = c->g; +struct commit_graph *g = data;uint32_t hash_version; hash_version =get_be32 (chunk_start);
@@ -415,14 +409,10 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, }if (s->commit_graph_changed_paths_version) { -struct graph_read_bloom_data_context context = { - .g = graph, - .commit_graph_changed_paths_version = &s->commit_graph_changed_paths_version - };pair_chunk (cf,GRAPH_CHUNKID_BLOOMINDEXES , &graph->chunk_bloom_indexes);read_chunk (cf,GRAPH_CHUNKID_BLOOMDATA , - graph_read_bloom_data, &context); + graph_read_bloom_data, graph); }if (graph->chunk_bloom_indexes && graph->chunk_bloom_data) {
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 15/17 object.h: fix mis-aligned flag bits table
In reply to Taylor BlauBit position 23 is one column too far to the left.
--- object.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
object.h +1 -1
@@ -62,7 +62,7 @@ void object_array_init(struct object_array *array);/* * object flag allocation: -* revision.h: 0---------10 15 23------27 +* revision.h: 0---------10 15 23------27 * fetch-pack.c: 01 67 * negotiator/default.c: 2--5 * walker.c: 0-2
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 16/17 commit-graph: reuse existing Bloom filters where possible
In reply to Taylor BlauIn 9e4df4da07 (commit-graph: new filter ver. that fixes murmur3, 2023-08-01), a bug was described where it's possible for Git to produce non-murmur3 hashes when the platform's "char" type is signed, and there are paths with characters whose highest bit is set (i.e. all characters
Quoted 1 line
=
0x80).
That patch allows the caller to control which version of Bloom filters are read and written. However, even on platforms with a signed "char" type, it is possible to reuse existing Bloom filters if and only if there are no changed paths in any commit's first parent tree-diff whose characters have their highest bit set.
When this is the case, we can reuse the existing filter without having to compute a new one. This is done by marking trees which are known to have (or not have) any such paths. When a commit's root tree is verified to not have any such paths, we mark it as such and declare that the commit's Bloom filter is reusable.
Note that this heuristic only goes in one direction. If neither a commit nor its first parent have any paths in their trees with non-ASCII characters, then we know for certain that a path with non-ASCII characters will not appear in a tree-diff against that commit's first parent. The reverse isn't necessarily true: just because the tree-diff doesn't contain any such paths does not imply that no such paths exist in either tree.
So we end up recomputing some Bloom filters that we don't strictly have to (i.e. their bits are the same no matter which version of murmur3 we use). But culling these out is impossible, since we'd have to perform the full tree-diff, which is the same effort as computing the Bloom filter from scratch.
But because we can cache our results in each tree's flag bits, we can often avoid recomputing many filters, thereby reducing the time it takes to run
$ git commit-graph write --changed-paths --reachable
when upgrading from v1 to v2 Bloom filters.
To benchmark this, let's generate a commit-graph in linux.git with v1 changed-paths in generation order[^1]:
$ git clone [email protected]:torvalds/linux.git $ cd linux $ git commit-graph write --reachable --changed-paths $ graph=".git/objects/info/commit-graph" $ mv $graph{,.bak}
Then let's time how long it takes to go from v1 to v2 filters (with and without the upgrade path enabled), resetting the state of the commit-graph each time:
$ git config commitGraph.changedPathsVersion 2
$ hyperfine -p 'cp -f $graph.bak $graph' -L v 0,1 \
'GIT_TEST_UPGRADE_BLOOM_FILTERS={v} git.compile commit-graph write --reachable --changed-paths'On linux.git (where there aren't any non-ASCII paths), the timings indicate that this patch represents a speed-up over recomputing all Bloom filters from scratch:
Benchmark 1: GIT_TEST_UPGRADE_BLOOM_FILTERS=0 git.compile commit-graph write --reachable --changed-paths
Time (mean ± σ): 124.873 s ± 0.316 s [User: 124.081 s, System: 0.643 s]
Range (min … max): 124.621 s … 125.227 s 3 runs
Benchmark 2: GIT_TEST_UPGRADE_BLOOM_FILTERS=1 git.compile commit-graph write --reachable --changed-paths
Time (mean ± σ): 79.271 s ± 0.163 s [User: 74.611 s, System: 4.521 s]
Range (min … max): 79.112 s … 79.437 s 3 runs
Summary
'GIT_TEST_UPGRADE_BLOOM_FILTERS=1 git.compile commit-graph write --reachable --changed-paths' ran
1.58 ± 0.01 times faster than 'GIT_TEST_UPGRADE_BLOOM_FILTERS=0 git.compile commit-graph write --reachable --changed-paths'On git.git, we do have some non-ASCII paths, giving us a more modest improvement from 4.163 seconds to 3.348 seconds, for a 1.24x speed-up. On my machine, the stats for git.git are:
- 8,285 Bloom filters computed from scratch
- 10 Bloom filters generated as empty
- 4 Bloom filters generated as truncated due to too many changed paths
- 65,114 Bloom filters were reused when transitioning from v1 to v2.
[^1]: Note that this is is important, since --stdin-packs or
--stdin-commits orders commits in the commit-graph by their pack position (with --stdin-packs) or in the raw input (with --stdin-commits).
Since we compute Bloom filters in the same order that commits appear in the graph, we must see a commit's (first) parent before we process the commit itself. This is only guaranteed to happen when sorting commits by their generation number.
--- bloom.c | 90 ++++++++++++++++++++++++++++++++++++++++++-- bloom.h | 1 + commit-graph.c | 5 +++ object.h | 1 + t/t4216-log-bloom.sh | 35 ++++++++++++++++- 5 files changed, 128 insertions(+), 4 deletions(-)
bloom.c +87 -3
@@ -7,6 +7,9 @@#include "commit-graph.h" #include "commit.h" #include "commit-slab.h" +#include "tree.h" +#include "tree-walk.h" +#include "config.h" define_commit_slab(bloom_filter_slab, struct bloom_filter);
@@ -250,6 +253,73 @@ static void init_truncated_large_filter(struct bloom_filter *filter, filter->version = version; } +#define VISITED (1u<<21) +#define HIGH_BITS (1u<<22) + +static int has_entries_with_high_bit (struct repository *r,struct tree *t) +{ +if (parse_tree (t)) +return 1 ; + +if (!(t->object.flags &VISITED )) { +struct tree_desc desc; +struct name_entry entry; + +init_tree_desc (&desc, t->buffer, t->size); +while (tree_entry (&desc, &entry)) { +size_t i; +for (i =0 ; i < entry.pathlen; i++) { +if (entry.path[i] &0x80 ) { + t->object.flags |=HIGH_BITS ; + goto done; + } + } + +if (S_ISDIR (entry.mode)) { +struct tree *sub =lookup_tree (r, &entry.oid); +if (sub &&has_entries_with_high_bit (r, sub)) { + t->object.flags |=HIGH_BITS ; + goto done; + } + } + + } + +done: + t->object.flags |=VISITED ; + } + +return !!(t->object.flags &HIGH_BITS ); +} + +static int commit_tree_has_high_bit_paths (struct repository *r, +struct commit *c) +{ +struct tree *t; +if (repo_parse_commit (r, c)) +return 1 ; + t =repo_get_commit_tree (r, c); +if (!t) +return 1 ; +return has_entries_with_high_bit (r, t); +} + +static struct bloom_filter *upgrade_filter (struct repository *r,struct commit *c, +struct bloom_filter *filter, +int hash_version) +{ +struct commit_list *p = c->parents; +if (commit_tree_has_high_bit_paths (r, c)) +return NULL ; + +if (p &&commit_tree_has_high_bit_paths (r, p->item)) +return NULL ; + + filter->version = hash_version; + +return filter; +} +struct bloom_filter *get_bloom_filter (struct repository *r,struct commit *c) {struct bloom_filter *filter;
@@ -292,9 +362,23 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, filter, graph_pos); } -if ((filter->data && filter->len) && - (!settings || settings->hash_version == filter->version)) -return filter; +if (filter->data && filter->len) { +struct bloom_filter *upgrade; +if (!settings || settings->hash_version == filter->version) +return filter; + +/* version mismatch, see if we can upgrade */ +if (compute_if_not_present && +git_env_bool ("GIT_TEST_UPGRADE_BLOOM_FILTERS" ,1 )) { + upgrade =upgrade_filter (r, c, filter, + settings->hash_version); +if (upgrade) { +if (computed) + *computed |=BLOOM_UPGRADED ; +return upgrade; + } + } + }if (!compute_if_not_present)return NULL ;
bloom.h +1 -0
@@ -102,6 +102,7 @@ enum bloom_filter_computed {BLOOM_COMPUTED = (1 <<1 ),BLOOM_TRUNC_LARGE = (1 <<2 ),BLOOM_TRUNC_EMPTY = (1 <<3 ), + BLOOM_UPGRADED = (1 << 4), }; struct bloom_filter *get_or_compute_bloom_filter(struct repository *r,
commit-graph.c +5 -0
@@ -1102,6 +1102,7 @@ struct write_commit_graph_context {int count_bloom_filter_not_computed;int count_bloom_filter_trunc_empty;int count_bloom_filter_trunc_large; +int count_bloom_filter_upgraded; }; static int write_graph_chunk_fanout(struct hashfile *f,
@@ -1709,6 +1710,8 @@ static void trace2_bloom_filter_write_statistics(struct write_commit_graph_conte ctx->count_bloom_filter_trunc_empty);trace2_data_intmax ("commit-graph" , ctx->r,"filter-trunc-large" , ctx->count_bloom_filter_trunc_large); +trace2_data_intmax ("commit-graph" , ctx->r,"filter-upgraded" , + ctx->count_bloom_filter_upgraded); }static void compute_bloom_filters (struct write_commit_graph_context *ctx)
@@ -1750,6 +1753,8 @@ static void compute_bloom_filters(struct write_commit_graph_context *ctx) ctx->count_bloom_filter_trunc_empty++;if (computed &BLOOM_TRUNC_LARGE ) ctx->count_bloom_filter_trunc_large++; + }else if (computed &BLOOM_UPGRADED ) { + ctx->count_bloom_filter_upgraded++; }else if (computed &BLOOM_NOT_COMPUTED ) ctx->count_bloom_filter_not_computed++; ctx->total_bloom_filter_data_size += filter
object.h +1 -0
@@ -75,6 +75,7 @@ void object_array_init(struct object_array *array);* commit-reach.c: 16-----19 * sha1-name.c: 20 * list-objects-filter.c: 21 +* bloom.c: 2122 * builtin/fsck.c: 0--3 * builtin/gc.c: 0 * builtin/index-pack.c: 2021
t/t4216-log-bloom.sh +34 -1
@@ -221,6 +221,10 @@ test_filter_trunc_large () { grep "\"key\":\"filter-trunc-large\",\"value\":\"$1\"" $2 } +test_filter_upgraded () { + grep "\"key\":\"filter-upgraded\",\"value\":\"$1\"" $2 +} + test_expect_success 'correctly report changes over limit' ' git init limits && (
@@ -628,7 +632,13 @@ test_expect_success 'when writing another commit graph, preserve existing versio test_expect_success 'when writing commit graph, do not reuse changed-path of another version' ' git init doublewrite && test_commit -C doublewrite c "$CENT" && + git -C doublewrite config --add commitgraph.changedPathsVersion 1 && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C doublewrite commit-graph write --reachable --changed-paths && + test_filter_computed 1 trace2.txt && + test_filter_upgraded 0 trace2.txt && + git -C doublewrite commit-graph write --reachable --changed-paths && for v in -2 3 do
@@ -639,8 +649,13 @@ test_expect_success 'when writing commit graph, do not reuse changed-path of ano EOF test_cmp expect err || return 1 done && + git -C doublewrite config --add commitgraph.changedPathsVersion 2 && - git -C doublewrite commit-graph write --reachable --changed-paths && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C doublewrite commit-graph write --reachable --changed-paths && + test_filter_computed 1 trace2.txt && + test_filter_upgraded 0 trace2.txt && + ( cd doublewrite && echo "c01f" >expect &&
@@ -649,4 +664,22 @@ test_expect_success 'when writing commit graph, do not reuse changed-path of ano ) ' +test_expect_success 'when writing commit graph, reuse changed-path of another version where possible' ' + git init upgrade && + + test_commit -C upgrade base no-high-bits && + + git -C upgrade config --add commitgraph.changedPathsVersion 1 && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C upgrade commit-graph write --reachable --changed-paths && + test_filter_computed 1 trace2.txt && + test_filter_upgraded 0 trace2.txt && + + git -C upgrade config --add commitgraph.changedPathsVersion 2 && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C upgrade commit-graph write --reachable --changed-paths && + test_filter_computed 0 trace2.txt && + test_filter_upgraded 1 trace2.txt +' + test_done
Signature · 1 line
2.42.0.415.g8942f205c8
PATCH v4 17/17 bloom: introduce `deinit_bloom_filters()`
In reply to Taylor BlauAfter we are done using Bloom filters, we do not currently clean up any memory allocated by the commit slab used to store those filters in the first place.
Besides the bloom_filter structures themselves, there is mostly nothing to free() in the first place, since in the read-only path all Bloom filter's data members point to a memory mapped region in the commit-graph file itself.
But when generating Bloom filters from scratch (or initializing truncated filters) we allocate additional memory to store the filter's data.
Keep track of when we need to free() this additional chunk of memory by using an extra pointer to_free. Most of the time this will be NULL (indicating that we are representing an existing Bloom filter stored in a memory mapped region). When it is non-NULL, free it before discarding the Bloom filters slab.
--- bloom.c | 16 +++++++++++++++- bloom.h | 3 +++ commit-graph.c | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-)
bloom.c +15 -1
@@ -59,6 +59,7 @@ int load_bloom_filter_from_graph(struct commit_graph *g,sizeof (unsigned char ) * start_index + BLOOMDATA_CHUNK_HEADER_SIZE); filter->version = g->bloom_filter_settings->hash_version; + filter->to_free =NULL ;return 1 ; }
@@ -231,6 +232,18 @@ void init_bloom_filters(void)init_bloom_filter_slab (&bloom_filters); } +static void free_one_bloom_filter (struct bloom_filter *filter) +{ +if (!filter) +return ; +free (filter->to_free); +} + +void deinit_bloom_filters (void ) +{ +deep_clear_bloom_filter_slab (&bloom_filters, free_one_bloom_filter); +} + static int pathmap_cmp(const void *hashmap_cmp_fn_data UNUSED, const struct hashmap_entry *eptr, const struct hashmap_entry *entry_or_key,
@@ -247,7 +260,7 @@ static int pathmap_cmp(const void *hashmap_cmp_fn_data UNUSED,static void init_truncated_large_filter (struct bloom_filter *filter,int version) { - filter->data =xmalloc (1 ); + filter->data = filter->to_free =xmalloc (1 ); filter->data[0 ] =0xFF ; filter->len =1 ; filter->version = version;
@@ -449,6 +462,7 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, filter->len =1 ; }CALLOC_ARRAY (filter->data, filter->len); + filter->to_free = filter->data;hashmap_for_each_entry (&pathmap, &iter, e, entry) {struct bloom_key key;
bloom.h +3 -0
@@ -56,6 +56,8 @@ struct bloom_filter {unsigned char *data;size_t len;int version; + +void *to_free; };/*
@@ -96,6 +98,7 @@ void add_key_to_filter(const struct bloom_key *key, const struct bloom_filter_settings *settings);void init_bloom_filters (void ); +void deinit_bloom_filters (void );enum bloom_filter_computed {BLOOM_NOT_COMPUTED = (1 <<0 ),
commit-graph.c +4 -0
@@ -779,6 +779,7 @@ struct bloom_filter_settings *get_bloom_filter_settings(struct repository *r)void close_commit_graph (struct raw_object_store *o) {clear_commit_graph_data_slab (&commit_graph_data_slab); +deinit_bloom_filters ();free_commit_graph (o->commit_graph); o->commit_graph =NULL ; }
@@ -2583,6 +2584,9 @@ int write_commit_graph(struct object_directory *odb, res =write_commit_graph_file (ctx); +if (ctx->changed_paths) +deinit_bloom_filters (); +if (ctx->split)mark_commit_graphs (ctx);
Signature · 1 line
2.42.0.415.g8942f205c8
Re: [PATCH v4 00/17] bloom: changed-path Bloom filters v2 (& sundries)
In reply to Taylor BlauQuoted 2 lines · Taylor Blau
(Rebased onto the tip of 'master', which is
3a06386e31(The fifteenth batch, 2023-10-04), at the time of writing).
Judging from 17/17 that has a free_commit_graph() call in close_commit_graph(), that was merged in the eighteenth batch, the above is probably untrue. I'll apply to the current master and see how it goes instead.
Quoted 4 lines
Thanks to Jonathan, Peff, and SZEDER who have helped a great deal in assembling these patches. As usual, a range-diff is included below. Thanks in advance for your
review!
Thanks.
Re: [PATCH v4 00/17] bloom: changed-path Bloom filters v2 (& sundries)
Quoted 9 lines · Junio C Hamano
Quoted 2 lines · Taylor Blau
(Rebased onto the tip of 'master', which is
3a06386e31(The fifteenth batch, 2023-10-04), at the time of writing).Judging from 17/17 that has a
free_commit_graph()call inclose_commit_graph(), that was merged in the eighteenth batch, the above is probably untrue. I'll apply to the current master and see how it goes instead.
Worse than that, I sent this --in-reply-to the wrong thread :-<.
Sorry about that, and indeed you are right that the correct base for this round should be a9ecda2788 (The eighteenth batch, 2023-10-13).
I'm optimistic that with the amount of careful review that this topic has already received, that this round should do the trick. But if there are more comments and we end up re-rolling it, I'll break this thread and split out the v5 into it's thread to avoid further confusion.
Quoted 6 lines
Quoted 4 lines
Thanks to Jonathan, Peff, and SZEDER who have helped a great deal in assembling these patches. As usual, a range-diff is included below. Thanks in advance for your
review!Thanks.
Thank you, and sorry for the mistake on my end.
Thanks,
Taylor
Re: [PATCH v4 00/17] bloom: changed-path Bloom filters v2 (& sundries)
Quoted 11 lines · Taylor Blau
Quoted 9 lines · Junio C Hamano
Quoted 2 lines · Taylor Blau
(Rebased onto the tip of 'master', which is
3a06386e31(The fifteenth batch, 2023-10-04), at the time of writing).Judging from 17/17 that has a
free_commit_graph()call inclose_commit_graph(), that was merged in the eighteenth batch, the above is probably untrue. I'll apply to the current master and see how it goes instead.
Quoted 7 lines
Worse than that, I sent this
--in-reply-tothe wrong thread :-<.Sorry about that, and indeed you are right that the correct base for this round should be
a9ecda2788(The eighteenth batch, 2023-10-13).I'm optimistic that with the amount of careful review that this topic has already received, that this round should do the trick.
Unfortunately, I can't share this optimism. This series still lacks tests exercising the interaction of different versions of Bloom filters and split commit graphs, and the one such test that I sent a while ago demonstrates that it's still broken. And it's getting
worse: back then I didn't send the related test that merged commit-graph layers containing different Bloom filter versions, because happened to succeed even back then; but, alas, with this series even that test fails.
Re: [PATCH v4 00/17] bloom: changed-path Bloom filters v2 (& sundries)
Quoted 4 lines · SZEDER Gábor
Quoted 2 lines · Taylor Blau
I'm optimistic that with the amount of careful review that this topic has already received, that this round should do the trick.
Quoted 8 lines
Unfortunately, I can't share this optimism. This series still lacks tests exercising the interaction of different versions of Bloom filters and split commit graphs, and the one such test that I sent a while ago demonstrates that it's still broken. And it's getting
worse: back then I didn't send the related test that merged commit-graph layers containing different Bloom filter versions, because happened to succeed even back then; but, alas, with this series even that test fails.
I am very confused here, the tests that you're referring to have been added to (and pass in) this series. What am I missing here?
Thanks,
Taylor
v5 cover bloom: changed-path Bloom filters v2 (& sundries)
In reply to Taylor Blau(Rebased onto the tip of 'master', which is d4dbce1db5 (The seventh batch, 2024-01-12), at the time of writing).
This series is a reroll of the combined efforts of [1] and [2] to introduce the v2 changed-path Bloom filters, which fixes a bug in our existing implementation of murmur3 paths with non-ASCII characters (when the "char" type is signed).
In large part, this is the same as the previous round. Like last time, this round addresses the remaining additional issues pointed out by SZEDER Gábor. The remaining issues which have been addressed by this series are:
- Incorrectly reading Bloom filters computed with differing hash versions. This has been corrected by discarding them when a version mismatch is detected.
- Added a note about the new commitGraph.changedPathVersion configuration variable which can cause (un-fixable, see [3]) issues in earlier versions of Git which do not yet understand them.
Thanks to Jonathan, Peff, and SZEDER who have helped a great deal in assembling these patches. As usual, a range-diff is included below.
Thanks in advance for your review!
[1]: https://lore.kernel.org/git/[email protected]/
[2]: https://lore.kernel.org/git/[email protected]/
[3]: https://lore.kernel.org/git/Zabr1Glljjgl%[email protected]/
Jonathan Tan (1):
gitformat-commit-graph: describe version 2 of BDAT
Taylor Blau (16):
t/t4216-log-bloom.sh: harden test_bloom_filters_not_used() revision.c: consult Bloom filters for root commits
commit-graph: ensure Bloom filters are read with consistent settings t/helper/test-read-graph.c: extract dump_graph_info() bloom.h: make load_bloom_filter_from_graph() public t/helper/test-read-graph: implement bloom-filters mode
t4216: test changed path filters with high bit paths
repo-settings: introduce commitgraph.changedPathsVersion
commit-graph: new Bloom filter version that fixes murmur3
bloom: annotate filters with hash version
bloom: prepare to discard incompatible Bloom filters commit-graph.c: unconditionally load Bloom filters
commit-graph: drop unnecessary graph_read_bloom_data_context object.h: fix mis-aligned flag bits table
commit-graph: reuse existing Bloom filters where possible
bloom: introduce deinit_bloom_filters()
Documentation/config/commitgraph.txt | 29 ++- Documentation/gitformat-commit-graph.txt | 9 +- bloom.c | 208 ++++++++++++++- bloom.h | 38 ++- commit-graph.c | 64 ++++- object.h | 3 +- oss-fuzz/fuzz-commit-graph.c | 2 +- repo-settings.c | 6 +- repository.h | 2 +- revision.c | 26 +- t/helper/test-bloom.c | 9 +- t/helper/test-read-graph.c | 67 ++++- t/t0095-bloom.sh | 8 + t/t4216-log-bloom.sh | 310 ++++++++++++++++++++++- 14 files changed, 724 insertions(+), 57 deletions(-)
Range-diff against v4:
1: e0fc51c3fb ! 1: c5e1b3e507 t/t4216-log-bloom.sh: harden test_bloom_filters_not_used()
@@ Commit message
checking for the absence of any data from it from trace2.
In the following commit, it will become possible to load Bloom filters
- without using them (e.g., because commitGraph.changedPathVersion is
- incompatible with the hash version with which the commit-graph's Bloom
- filters were written).
+ without using them (e.g., because the commitGraph.changedPathVersion
+ introduced later in this series is incompatible with the hash version
+ with which the commit-graph's Bloom filters were written).
When this is the case, it's possible to initialize the Bloom filter sub-system, while still not using any Bloom filters. When this is the
2: 87b09e6266 ! 2: 8f32fd5f46 revision.c: consult Bloom filters for root commits
@@ revision.c: static int rev_compare_tree(struct rev_info *revs,
+ int nth_parent)
{
struct tree *t1 = repo_get_commit_tree(the_repository, commit);
-+ int bloom_ret = 1;
++ int bloom_ret = -1;
if (!t1)
return 0;
-+ if (nth_parent == 1 && revs->bloom_keys_nr) {
++ if (!nth_parent && revs->bloom_keys_nr) {
+ bloom_ret = check_maybe_different_in_bloom_filter(revs, commit);
+ if (!bloom_ret)
+ return 1;
@@ revision.c: static void try_to_simplify_commit(struct rev_info *revs, struct com
+ * (if one exists) is relative to the empty tree, using Bloom
+ * filters is allowed here.
+ */
-+ if (rev_same_tree_as_empty(revs, commit, 1))
++ if (rev_same_tree_as_empty(revs, commit, 0))
commit->object.flags |= TREESAME;
return;
}
3: 46d8a41005 ! 3: 285b25f1b7 commit-graph: ensure Bloom filters are read with consistent settings
@@ t/t4216-log-bloom.sh: test_expect_success 'Bloom generation backfills empty comm
+ test_must_be_empty err
+'
+
- test_done
+ corrupt_graph () {
+- graph=.git/objects/info/commit-graph &&
+ test_when_finished "rm -rf $graph" &&
+ git commit-graph write --reachable --changed-paths &&
+ corrupt_chunk_file $graph "$@"
4: 4d0190a992 = 4: 0cee8078d4 gitformat-commit-graph: describe version 2 of BDAT
5: 3c2057c11c = 5: 1fc8d2828d t/helper/test-read-graph.c: extract `dump_graph_info()`
6: e002e35004 ! 6: 03dd7cf30a bloom.h: make `load_bloom_filter_from_graph()` public@@ Commit message
## bloom.c ##
-@@ bloom.c: static inline unsigned char get_bitmask(uint32_t pos)
- return ((unsigned char)1) << (pos & (BITS_PER_WORD - 1));
+@@ bloom.c: static int check_bloom_offset(struct commit_graph *g, uint32_t pos,
+ return -1;
}
-static int load_bloom_filter_from_graph(struct commit_graph *g,
7: c7016f51cd = 7: dd9193e404 t/helper/test-read-graph: implement `bloom-filters` mode 8: cef2aac8ba ! 8: aa2416795d t4216: test changed path filters with high bit paths
@@
## Metadata ##
-Author: Jonathan Tan <[email protected]> +Author: Taylor Blau <[email protected]>
## Commit message ##
t4216: test changed path filters with high bit paths
@@ t/t4216-log-bloom.sh: test_expect_success 'merge graph layers with incompatible
+ )
+'
+
- test_done
+ corrupt_graph () {
+ test_when_finished "rm -rf $graph" &&
+ git commit-graph write --reachable --changed-paths &&
9: 36d4e2202e ! 9: a77dcc99b4 repo-settings: introduce commitgraph.changedPathsVersion
@@
## Metadata ##
-Author: Jonathan Tan <[email protected]>
+Author: Taylor Blau <[email protected]>
## Commit message ##
repo-settings: introduce commitgraph.changedPathsVersion
@@ Documentation/config/commitgraph.txt: commitGraph.maxNewFilters::
+
+commitGraph.changedPathsVersion::
+ Specifies the version of the changed-path Bloom filters that Git will read and
-+ write. May be -1, 0 or 1.
++ write. May be -1, 0 or 1. Note that values greater than 1 may be
++ incompatible with older versions of Git which do not yet understand
++ those versions. Use caution when operating in a mixed-version
++ environment.
++
+Defaults to -1.
++
@@ commit-graph.c: struct commit_graph *parse_commit_graph(struct repo_settings *s,
- if (s->commit_graph_read_changed_paths) {
+ if (s->commit_graph_changed_paths_version) {
- pair_chunk(cf, GRAPH_CHUNKID_BLOOMINDEXES,
- &graph->chunk_bloom_indexes);
+ read_chunk(cf, GRAPH_CHUNKID_BLOOMINDEXES,
+ graph_read_bloom_index, graph);
read_chunk(cf, GRAPH_CHUNKID_BLOOMDATA,
+@@ commit-graph.c: static void validate_mixed_bloom_settings(struct commit_graph *g)
+ }
+
+ if (g->bloom_filter_settings->bits_per_entry != settings->bits_per_entry ||
+- g->bloom_filter_settings->num_hashes != settings->num_hashes) {
++ g->bloom_filter_settings->num_hashes != settings->num_hashes ||
++ g->bloom_filter_settings->hash_version != settings->hash_version) {
+ g->chunk_bloom_indexes = NULL;
+ g->chunk_bloom_data = NULL;
+ FREE_AND_NULL(g->bloom_filter_settings);
## oss-fuzz/fuzz-commit-graph.c ##
@@ oss-fuzz/fuzz-commit-graph.c: int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
@@ repository.h: struct repo_settings {
int gc_write_commit_graph;
int fetch_write_commit_graph;
int command_requires_full_index;
+
+ ## t/t4216-log-bloom.sh ##
+@@ t/t4216-log-bloom.sh: test_expect_success 'setup for mixed Bloom setting tests' '
+ done
+ '
+
+-test_expect_success 'ensure incompatible Bloom filters are ignored' '
++test_expect_success 'ensure Bloom filters with incompatible settings are ignored' '
+ # Compute Bloom filters with "unusual" settings.
+ git -C $repo rev-parse one >in &&
+ GIT_TEST_BLOOM_SETTINGS_NUM_HASHES=3 git -C $repo commit-graph write \
+@@ t/t4216-log-bloom.sh: test_expect_success 'merge graph layers with incompatible Bloom settings' '
+ test_must_be_empty err
+ '
+
++test_expect_success 'ensure Bloom filter with incompatible versions are ignored' '
++ rm "$repo/$graph" &&
++
++ git -C $repo log --oneline --no-decorate -- $CENT >expect &&
++
++ # Compute v1 Bloom filters for commits at the bottom.
++ git -C $repo rev-parse HEAD^ >in &&
++ git -C $repo commit-graph write --stdin-commits --changed-paths \
++ --split <in &&
++
++ # Compute v2 Bloomfilters for the rest of the commits at the top.
++ git -C $repo rev-parse HEAD >in &&
++ git -C $repo -c commitGraph.changedPathsVersion=2 commit-graph write \
++ --stdin-commits --changed-paths --split=no-merge <in &&
++
++ test_line_count = 2 $repo/$chain &&
++
++ git -C $repo log --oneline --no-decorate -- $CENT >actual 2>err &&
++ test_cmp expect actual &&
++
++ layer="$(head -n 1 $repo/$chain)" &&
++ cat >expect.err <<-EOF &&
++ warning: disabling Bloom filters for commit-graph layer $SQ$layer$SQ due to incompatible settings
++ EOF
++ test_cmp expect.err err
++'
++
+ get_first_changed_path_filter () {
+ test-tool read-graph bloom-filters >filters.dat &&
+ head -n 1 filters.dat10: f6ab427ead ! 10: f0f22e852c commit-graph: new filter ver. that fixes murmur3
@@
## Metadata ##
-Author: Jonathan Tan <[email protected]>
+Author: Taylor Blau <[email protected]>
## Commit message ##
- commit-graph: new filter ver. that fixes murmur3
+ commit-graph: new Bloom filter version that fixes murmur3
The murmur3 implementation in bloom.c has a bug when converting series of 4 bytes into network-order integers when char is signed (which is
@@ Documentation/config/commitgraph.txt: commitGraph.readChangedPaths::
commitGraph.changedPathsVersion::
Specifies the version of the changed-path Bloom filters that Git will read and
-- write. May be -1, 0 or 1.
-+ write. May be -1, 0, 1, or 2.
- +
- Defaults to -1.
- +
+- write. May be -1, 0 or 1. Note that values greater than 1 may be
++ write. May be -1, 0, 1, or 2. Note that values greater than 1 may be
+ incompatible with older versions of Git which do not yet understand
+ those versions. Use caution when operating in a mixed-version
+ environment.
@@ Documentation/config/commitgraph.txt: filters when instructed to write.
If 1, Git will only read version 1 Bloom filters, and will write version 1 Bloom filters.
@@ bloom.h: int load_bloom_filter_from_graph(struct commit_graph *g,
size_t len,
## commit-graph.c ##
-@@ commit-graph.c: static int graph_read_oid_lookup(const unsigned char *chunk_start, +@@ commit-graph.c: static int graph_read_bloom_index(const unsigned char *chunk_start,
return 0;
}
@@ commit-graph.c: static int graph_read_oid_lookup(const unsigned char *chunk_star
+ struct graph_read_bloom_data_context *c = data;
+ struct commit_graph *g = c->g;
uint32_t hash_version;
-- g->chunk_bloom_data = chunk_start;
- hash_version = get_be32(chunk_start);
-- if (hash_version != 1)
-+ if (*c->commit_graph_changed_paths_version == -1) {
+ if (chunk_size < BLOOMDATA_CHUNK_HEADER_SIZE) {
+@@ commit-graph.c: static int graph_read_bloom_data(const unsigned char *chunk_start,
+ return -1;
+ }
+
++ hash_version = get_be32(chunk_start);
++
++ if (*c->commit_graph_changed_paths_version == -1)
+ *c->commit_graph_changed_paths_version = hash_version;
-+ } else if (hash_version != *c->commit_graph_changed_paths_version) {
- return 0;
-+ }
-
-+ g->chunk_bloom_data = chunk_start;
++ else if (hash_version != *c->commit_graph_changed_paths_version)
++ return 0;
++
+ g->chunk_bloom_data = chunk_start;
+ g->chunk_bloom_data_size = chunk_size;
+- hash_version = get_be32(chunk_start);
+-
+- if (hash_version != 1)
+- return 0;
+-
g->bloom_filter_settings = xmalloc(sizeof(struct bloom_filter_settings)); g->bloom_filter_settings->hash_version = hash_version; g->bloom_filter_settings->num_hashes = get_be32(chunk_start + 4);
@@ commit-graph.c: struct commit_graph *parse_commit_graph(struct repo_settings *s,
+ .g = graph,
+ .commit_graph_changed_paths_version = &s->commit_graph_changed_paths_version
+ };
- pair_chunk(cf, GRAPH_CHUNKID_BLOOMINDEXES,
- &graph->chunk_bloom_indexes);
+ read_chunk(cf, GRAPH_CHUNKID_BLOOMINDEXES,
+ graph_read_bloom_index, graph);
read_chunk(cf, GRAPH_CHUNKID_BLOOMDATA,
- graph_read_bloom_data, graph);
+ graph_read_bloom_data, &context);
@@ t/t4216-log-bloom.sh: test_expect_success 'version 1 changed-path used when vers
+ )
+'
+
- test_done
+ corrupt_graph () {
+ test_when_finished "rm -rf $graph" &&
+ git commit-graph write --reachable --changed-paths &&
11: dc69b28329 = 11: b56e94cad7 bloom: annotate filters with hash version 12: 85dbdc4ed2 = 12: ddfd1ba32a bloom: prepare to discard incompatible Bloom filters 13: 3ff669a622 ! 13: 72aabd289b commit-graph.c: unconditionally load Bloom filters
@@ Metadata
## Commit message ##
commit-graph.c: unconditionally load Bloom filters
- In 9e4df4da07 (commit-graph: new filter ver. that fixes murmur3,
- 2023-08-01), we began ignoring the Bloom data ("BDAT") chunk for
- commit-graphs whose Bloom filters were computed using a hash version
- incompatible with the value of commitGraph.changedPathVersion.
+ In an earlier commit, we began ignoring the Bloom data ("BDAT") chunk
+ for commit-graphs whose Bloom filters were computed using a hash version
+ incompatible with the value of commitGraph.changedPathVersion.
Now that the Bloom API has been hardened to discard these incompatible filters (with the exception of low-level APIs), we can safely load these
@@ Commit message
## commit-graph.c ##
@@ commit-graph.c: static int graph_read_bloom_data(const unsigned char *chunk_start,
- uint32_t hash_version;
+
hash_version = get_be32(chunk_start);
-- if (*c->commit_graph_changed_paths_version == -1) {
+- if (*c->commit_graph_changed_paths_version == -1)
- *c->commit_graph_changed_paths_version = hash_version;
-- } else if (hash_version != *c->commit_graph_changed_paths_version) {
+- else if (hash_version != *c->commit_graph_changed_paths_version)
- return 0;
-- }
-
g->chunk_bloom_data = chunk_start;
+ g->chunk_bloom_data_size = chunk_size;
g->bloom_filter_settings = xmalloc(sizeof(struct bloom_filter_settings));
- g->bloom_filter_settings->hash_version = hash_version;
@@ commit-graph.c: int write_commit_graph(struct object_directory *odb,
ctx->write_generation_data = (get_configured_generation_version(r) == 2); ctx->num_generation_data_overflows = 0;
14: 1c78e3d178 ! 14: 526beb9766 commit-graph: drop unnecessary graph_read_bloom_data_context
@@ Commit message
## commit-graph.c ##
-@@ commit-graph.c: static int graph_read_oid_lookup(const unsigned char *chunk_start, +@@ commit-graph.c: static int graph_read_bloom_index(const unsigned char *chunk_start,
return 0;
}
@@ commit-graph.c: static int graph_read_oid_lookup(const unsigned char *chunk_star
- struct commit_graph *g = c->g;
+ struct commit_graph *g = data;
uint32_t hash_version;
- hash_version = get_be32(chunk_start);
+ if (chunk_size < BLOOMDATA_CHUNK_HEADER_SIZE) {
@@ commit-graph.c: struct commit_graph *parse_commit_graph(struct repo_settings *s,
}
@@ commit-graph.c: struct commit_graph *parse_commit_graph(struct repo_settings *s,
- .g = graph,
- .commit_graph_changed_paths_version = &s->commit_graph_changed_paths_version
- };
- pair_chunk(cf, GRAPH_CHUNKID_BLOOMINDEXES,
- &graph->chunk_bloom_indexes);
+ read_chunk(cf, GRAPH_CHUNKID_BLOOMINDEXES,
+ graph_read_bloom_index, graph);
read_chunk(cf, GRAPH_CHUNKID_BLOOMDATA,
- graph_read_bloom_data, &context);
+ graph_read_bloom_data, graph);
15: a289514faa = 15: c683697efa object.h: fix mis-aligned flag bits table 16: 6a12e39e7f ! 16: 4bf043be9a commit-graph: reuse existing Bloom filters where possible
@@ Metadata
## Commit message ##
commit-graph: reuse existing Bloom filters where possible
- In 9e4df4da07 (commit-graph: new filter ver. that fixes murmur3,
- 2023-08-01), a bug was described where it's possible for Git to produce
- non-murmur3 hashes when the platform's "char" type is signed, and there
- are paths with characters whose highest bit is set (i.e. all characters
- >= 0x80).
+ In an earlier commit, a bug was described where it's possible for Git to
+ produce non-murmur3 hashes when the platform's "char" type is signed,
+ and there are paths with characters whose highest bit is set (i.e. all
+ characters >= 0x80).
That patch allows the caller to control which version of Bloom filters are read and written. However, even on platforms with a signed "char"
@@ t/t4216-log-bloom.sh: test_expect_success 'when writing commit graph, do not reu
+ test_filter_upgraded 1 trace2.txt
+'
+
- test_done
+ corrupt_graph () {
+ test_when_finished "rm -rf $graph" &&
+ git commit-graph write --reachable --changed-paths &&
17: 8942f205c8 = 17: 7daa0d8833 bloom: introduce deinit_bloom_filters()
base-commit: d4dbce1db5cd227a57074bcfc7ec9f0655961bba
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 1/17 t/t4216-log-bloom.sh: harden `test_bloom_filters_not_used()`
The existing implementation of test_bloom_filters_not_used() asserts that the Bloom filter sub-system has not been initialized at all, by checking for the absence of any data from it from trace2.
In the following commit, it will become possible to load Bloom filters without using them (e.g., because the commitGraph.changedPathVersion introduced later in this series is incompatible with the hash version with which the commit-graph's Bloom filters were written).
When this is the case, it's possible to initialize the Bloom filter sub-system, while still not using any Bloom filters. When this is the case, check that the data dump from the Bloom sub-system is all zeros, indicating that no filters were used.
--- t/t4216-log-bloom.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-)
t/t4216-log-bloom.sh +13 -1
@@ -82,7 +82,19 @@ test_bloom_filters_used () { test_bloom_filters_not_used () { log_args=$1 setup "$log_args" && - ! grep -q "statistics:{\"filter_not_present\":" "$TRASH_DIRECTORY/trace.perf" && + + if grep -q "statistics:{\"filter_not_present\":" "$TRASH_DIRECTORY/trace.perf" + then + # if the Bloom filter system is initialized, ensure that no + # filters were used + data="statistics:{" + data="$data\"filter_not_present\":0," + data="$data\"maybe\":0," + data="$data\"definitely_not\":0," + data="$data\"false_positive\":0}" + + grep -q "$data" "$TRASH_DIRECTORY/trace.perf" + fi && test_cmp log_wo_bloom log_w_bloom }
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 2/17 revision.c: consult Bloom filters for root commits
In reply to Taylor BlauThe commit-graph stores changed-path Bloom filters which represent the set of paths included in a tree-level diff between a commit's root tree and that of its parent.
When a commit has no parents, the tree-diff is computed against that commit's root tree and the empty tree. In other words, every path in that commit's tree is stored in the Bloom filter (since they all appear in the diff).
Consult these filters during pathspec-limited traversals in the function rev_same_tree_as_empty(). Doing so yields a performance improvement where we can avoid enumerating the full set of paths in a parentless commit's root tree when we know that the path(s) of interest were not listed in that commit's changed-path Bloom filter.
--- revision.c | 26 ++++++++++++++++++++++---- t/t4216-log-bloom.sh | 8 ++++++-- 2 files changed, 28 insertions(+), 6 deletions(-)
revision.c +22 -4
@@ -833,17 +833,28 @@ static int rev_compare_tree(struct rev_info *revs,return tree_difference; } -static int rev_same_tree_as_empty (struct rev_info *revs,struct commit *commit) +static int rev_same_tree_as_empty (struct rev_info *revs,struct commit *commit, +int nth_parent) {struct tree *t1 =repo_get_commit_tree (the_repository, commit); +int bloom_ret =-1 ;if (!t1)return 0 ; +if (!nth_parent && revs->bloom_keys_nr) { + bloom_ret =check_maybe_different_in_bloom_filter (revs, commit); +if (!bloom_ret) +return 1 ; + } + tree_difference =REV_TREE_SAME ; revs->pruning.flags.has_changes =0 ;diff_tree_oid (NULL , &t1->object.oid,"" , &revs->pruning); +if (bloom_ret ==1 && tree_difference ==REV_TREE_SAME ) + count_bloom_filter_false_positive++; +return tree_difference ==REV_TREE_SAME ; }
@@ -881,7 +892,7 @@ static int compact_treesame(struct rev_info *revs, struct commit *commit, unsignif (nth_parent !=0 )die ("compact_treesame %u" , nth_parent); old_same = !!(commit->object.flags &TREESAME ); -if (rev_same_tree_as_empty (revs, commit)) +if (rev_same_tree_as_empty (revs, commit, nth_parent)) commit->object.flags |=TREESAME ;else commit->object.flags &= ~TREESAME ;
@@ -977,7 +988,14 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit)return ;if (!commit->parents) { -if (rev_same_tree_as_empty (revs, commit)) +/* +* Pretend as if we are comparing ourselves to the +* (non-existent) first parent of this commit object. Even +* though no such parent exists, its changed-path Bloom filter +* (if one exists) is relative to the empty tree, using Bloom +* filters is allowed here. +*/ +if (rev_same_tree_as_empty (revs, commit,0 )) commit->object.flags |=TREESAME ;return ; }
@@ -1058,7 +1076,7 @@ static void try_to_simplify_commit(struct rev_info *revs, struct commit *commit)case REV_TREE_NEW :if (revs->remove_empty_trees && -rev_same_tree_as_empty (revs, p)) { +rev_same_tree_as_empty (revs, p, nth_parent)) {/* We are adding all the specified * paths from this parent, so the * history beyond this parent is not
t/t4216-log-bloom.sh +6 -2
@@ -88,7 +88,11 @@ test_bloom_filters_not_used () { # if the Bloom filter system is initialized, ensure that no # filters were used data="statistics:{" - data="$data\"filter_not_present\":0," + # unusable filters (e.g., those computed with a + # different value of commitGraph.changedPathsVersion) + # are counted in the filter_not_present bucket, so any + # value is OK there. + data="$data\"filter_not_present\":[0-9][0-9]*," data="$data\"maybe\":0," data="$data\"definitely_not\":0," data="$data\"false_positive\":0}"
@@ -175,7 +179,7 @@ test_expect_success 'setup - add commit-graph to the chain with Bloom filters' ' test_bloom_filters_used_when_some_filters_are_missing () { log_args=$1 - bloom_trace_prefix="statistics:{\"filter_not_present\":3,\"maybe\":6,\"definitely_not\":9" + bloom_trace_prefix="statistics:{\"filter_not_present\":3,\"maybe\":6,\"definitely_not\":10" setup "$log_args" && grep -q "$bloom_trace_prefix" "$TRASH_DIRECTORY/trace.perf" && test_cmp log_wo_bloom log_w_bloom
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 3/17 commit-graph: ensure Bloom filters are read with consistent settings
In reply to Taylor BlauThe changed-path Bloom filter mechanism is parameterized by a couple of variables, notably the number of bits per hash (typically "m" in Bloom filter literature) and the number of hashes themselves (typically "k").
It is critically important that filters are read with the Bloom filter settings that they were written with. Failing to do so would mean that each query is liable to compute different fingerprints, meaning that the filter itself could return a false negative. This goes against a basic assumption of using Bloom filters (that they may return false positives, but never false negatives) and can lead to incorrect results.
We have some existing logic to carry forward existing Bloom filter settings from one layer to the next. In write_commit_graph(), we have something like:
if (!(flags & COMMIT_GRAPH_NO_WRITE_BLOOM_FILTERS)) {
struct commit_graph *g = ctx->r->objects->commit_graph;
/* We have changed-paths already. Keep them in the next graph */
if (g && g->chunk_bloom_data) {
ctx->changed_paths = 1;
ctx->bloom_settings = g->bloom_filter_settings;
}
}, which drags forward Bloom filter settings across adjacent layers.
This doesn't quite address all cases, however, since it is possible for intermediate layers to contain no Bloom filters at all. For example, suppose we have two layers in a commit-graph chain, say, {G1, G2}. If G1 contains Bloom filters, but G2 doesn't, a new G3 (whose base graph is G2) may be written with arbitrary Bloom filter settings, because we only check the immediately adjacent layer's settings for compatibility.
This behavior has existed since the introduction of changed-path Bloom filters. But in practice, this is not such a big deal, since the only way up until this point to modify the Bloom filter settings at write time is with the undocumented environment variables:
- GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY
- GIT_TEST_BLOOM_SETTINGS_NUM_HASHES
- GIT_TEST_BLOOM_SETTINGS_MAX_CHANGED_PATHS
(it is still possible to tweak MAX_CHANGED_PATHS between layers, but this does not affect reads, so is allowed to differ across multiple graph layers).
But in future commits, we will introduce another parameter to change the hash algorithm used to compute Bloom fingerprints itself. This will be exposed via a configuration setting, making this foot-gun easier to use.
To prevent this potential issue, validate that all layers of a split commit-graph have compatible settings with the newest layer which contains Bloom filters.
--- commit-graph.c | 25 +++++++++++++++++ t/t4216-log-bloom.sh | 65 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-)
commit-graph.c +25 -0
@@ -543,6 +543,30 @@ static int validate_mixed_generation_chain(struct commit_graph *g)return 0 ; } +static void validate_mixed_bloom_settings (struct commit_graph *g) +{ +struct bloom_filter_settings *settings =NULL ; +for (; g; g = g->base_graph) { +if (!g->bloom_filter_settings) +continue ; +if (!settings) { + settings = g->bloom_filter_settings; +continue ; + } + +if (g->bloom_filter_settings->bits_per_entry != settings->bits_per_entry || + g->bloom_filter_settings->num_hashes != settings->num_hashes) { + g->chunk_bloom_indexes =NULL ; + g->chunk_bloom_data =NULL ; +FREE_AND_NULL (g->bloom_filter_settings); + +warning (_ ("disabling Bloom filters for commit-graph " +"layer '%s' due to incompatible settings" ), +oid_to_hex (&g->oid)); + } + } +} + static int add_graph_to_chain(struct commit_graph *g, struct commit_graph *chain, struct object_id *oids,
@@ -666,6 +690,7 @@ struct commit_graph *load_commit_graph_chain_fd_st(struct repository *r, }validate_mixed_generation_chain (graph_chain); +validate_mixed_bloom_settings (graph_chain);free (oids);fclose (fp);
t/t4216-log-bloom.sh +64 -1
72 lines not on this page · raw message
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 4/17 gitformat-commit-graph: describe version 2 of BDAT
In reply to Taylor BlauFrom: Jonathan Tan <[email protected]>
The code change to Git to support version 2 will be done in subsequent commits.
--- Documentation/gitformat-commit-graph.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-)
Documentation/gitformat-commit-graph.txt +6 -3
@@ -142,13 +142,16 @@ All multi-byte numbers are in network byte order. ==== Bloom Filter Data (ID: {'B', 'D', 'A', 'T'}) [Optional] * It starts with header consisting of three unsigned 32-bit integers: - - Version of the hash algorithm being used. We currently only support - value 1 which corresponds to the 32-bit version of the murmur3 hash + - Version of the hash algorithm being used. We currently support + value 2 which corresponds to the 32-bit version of the murmur3 hash implemented exactly as described in https://en.wikipedia.org/wiki/MurmurHash#Algorithm and the double hashing technique using seed values 0x293ae76f and 0x7e646e2 as described in https://doi.org/10.1007/978-3-540-30494-4_26 "Bloom Filters - in Probabilistic Verification" + in Probabilistic Verification". Version 1 Bloom filters have a bug that appears + when char is signed and the repository has path names that have characters >= + 0x80; Git supports reading and writing them, but this ability will be removed + in a future version of Git. - The number of times a path is hashed and hence the number of bit positions that cumulatively determine whether a file is present in the commit. - The minimum number of bits 'b' per entry in the Bloom filter. If the filter
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 5/17 t/helper/test-read-graph.c: extract `dump_graph_info()`
In reply to Taylor BlauPrepare for the 'read-graph' test helper to perform other tasks besides dumping high-level information about the commit-graph by extracting its main routine into a separate function.
--- t/helper/test-read-graph.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-)
t/helper/test-read-graph.c +18 -13
@@ -5,20 +5,8 @@#include "bloom.h" #include "setup.h" -int cmd__read_graph(int argc UNUSED, const char **argv UNUSED) +static void dump_graph_info (struct commit_graph *graph) { -struct commit_graph *graph =NULL ; -struct object_directory *odb; - -setup_git_directory (); - odb = the_repository->objects->odb; - -prepare_repo_settings (the_repository); - - graph =read_commit_graph_one (the_repository, odb); -if (!graph) -return 1 ; - printf("header: %08x %d %d %d %d\n", ntohl(*(uint32_t*)graph->data), *(unsigned char*)(graph->data + 4),
@@ -57,6 +45,23 @@ int cmd__read_graph(int argc UNUSED, const char **argv UNUSED)if (graph->topo_levels)printf (" topo_levels" );printf ("\n" ); +} + +int cmd__read_graph(int argc UNUSED, const char **argv UNUSED) +{ +struct commit_graph *graph =NULL ; +struct object_directory *odb; + +setup_git_directory (); + odb = the_repository->objects->odb; + +prepare_repo_settings (the_repository); + + graph =read_commit_graph_one (the_repository, odb); +if (!graph) +return 1 ; + +dump_graph_info (graph);UNLEAK (graph);
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 6/17 bloom.h: make `load_bloom_filter_from_graph()` public
In reply to Taylor BlauPrepare for a future commit to use the load_bloom_filter_from_graph() function directly to load specific Bloom filters out of the commit-graph for manual inspection (to be used during tests).
--- bloom.c | 6 +++--- bloom.h | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-)
bloom.c +3 -3
@@ -48,9 +48,9 @@ static int check_bloom_offset(struct commit_graph *g, uint32_t pos,return -1 ; } -static int load_bloom_filter_from_graph (struct commit_graph *g, -struct bloom_filter *filter, -uint32_t graph_pos) +int load_bloom_filter_from_graph (struct commit_graph *g, +struct bloom_filter *filter, +uint32_t graph_pos) {uint32_t lex_pos, start_index, end_index;
bloom.h +5 -0
@@ -3,6 +3,7 @@struct commit ;struct repository ; +struct commit_graph ;struct bloom_filter_settings {/*
@@ -68,6 +69,10 @@ struct bloom_key {uint32_t *hashes; }; +int load_bloom_filter_from_graph (struct commit_graph *g, +struct bloom_filter *filter, +uint32_t graph_pos); +/* * Calculate the murmur3 32-bit hash value for the given data * using the given seed.
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 7/17 t/helper/test-read-graph: implement `bloom-filters` mode
In reply to Taylor BlauImplement a mode of the "read-graph" test helper to dump out the hexadecimal contents of the Bloom filter(s) contained in a commit-graph.
--- t/helper/test-read-graph.c | 44 +++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-)
t/helper/test-read-graph.c +39 -5
@@ -47,10 +47,32 @@ static void dump_graph_info(struct commit_graph *graph)printf ("\n" ); } -int cmd__read_graph(int argc UNUSED, const char **argv UNUSED) +static void dump_graph_bloom_filters (struct commit_graph *graph) +{ +uint32_t i; + +for (i =0 ; i < graph->num_commits + graph->num_commits_in_base; i++) { +struct bloom_filter filter = {0 }; +size_t j; + +if (load_bloom_filter_from_graph (graph, &filter, i) <0 ) { +fprintf (stderr,"missing Bloom filter for graph " +"position %" PRIu32"\n" , i); +continue ; + } + +for (j =0 ; j < filter.len; j++) +printf ("%02x" , filter.data[j]); +if (filter.len) +printf ("\n" ); + } +} + +int cmd__read_graph (int argc,const char **argv) {struct commit_graph *graph =NULL ;struct object_directory *odb; +int ret =0 ;setup_git_directory (); odb = the_repository->objects->odb;
@@ -58,12 +80,24 @@ int cmd__read_graph(int argc UNUSED, const char **argv UNUSED)prepare_repo_settings (the_repository); graph =read_commit_graph_one (the_repository, odb); -if (!graph) -return 1 ; +if (!graph) { + ret =1 ; + goto done; + } -dump_graph_info (graph); +if (argc <=1 ) +dump_graph_info (graph); +else if (!strcmp (argv[1 ],"bloom-filters" )) +dump_graph_bloom_filters (graph); +else { +fprintf (stderr,"unknown sub-command: '%s'\n" , argv[1 ]); + ret =1 ; + } +done:UNLEAK (graph); -return 0 ; +return ret; } + +
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 8/17 t4216: test changed path filters with high bit paths
In reply to Taylor BlauSubsequent commits will teach Git another version of changed path filter that has different behavior with paths that contain at least one character with its high bit set, so test the existing behavior as a baseline.
--- t/t4216-log-bloom.sh | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+)
t/t4216-log-bloom.sh +51 -0
@@ -485,6 +485,57 @@ test_expect_success 'merge graph layers with incompatible Bloom settings' ' test_must_be_empty err ' +get_first_changed_path_filter () { + test-tool read-graph bloom-filters >filters.dat && + head -n 1 filters.dat +} + +# chosen to be the same under all Unicode normalization forms +CENT=$(printf "\302\242") + +test_expect_success 'set up repo with high bit path, version 1 changed-path' ' + git init highbit1 && + test_commit -C highbit1 c1 "$CENT" && + git -C highbit1 commit-graph write --reachable --changed-paths +' + +test_expect_success 'setup check value of version 1 changed-path' ' + ( + cd highbit1 && + echo "52a9" >expect && + get_first_changed_path_filter >actual + ) +' + +# expect will not match actual if char is unsigned by default. Write the test +# in this way, so that a user running this test script can still see if the two +# files match. (It will appear as an ordinary success if they match, and a skip +# if not.) +if test_cmp highbit1/expect highbit1/actual +then + test_set_prereq SIGNED_CHAR_BY_DEFAULT +fi +test_expect_success SIGNED_CHAR_BY_DEFAULT 'check value of version 1 changed-path' ' + # Only the prereq matters for this test. + true +' + +test_expect_success 'setup make another commit' ' + # "git log" does not use Bloom filters for root commits - see how, in + # revision.c, rev_compare_tree() (the only code path that eventually calls + # get_bloom_filter()) is only called by try_to_simplify_commit() when the commit + # has one parent. Therefore, make another commit so that we perform the tests on + # a non-root commit. + test_commit -C highbit1 anotherc1 "another$CENT" +' + +test_expect_success 'version 1 changed-path used when version 1 requested' ' + ( + cd highbit1 && + test_bloom_filters_used "-- another$CENT" + ) +' + corrupt_graph () { test_when_finished "rm -rf $graph" && git commit-graph write --reachable --changed-paths &&
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 9/17 repo-settings: introduce commitgraph.changedPathsVersion
In reply to Taylor BlauA subsequent commit will introduce another version of the changed-path filter in the commit graph file. In order to control which version to write (and read), a config variable is needed.
Therefore, introduce this config variable. For forwards compatibility, teach Git to not read commit graphs when the config variable is set to an unsupported version. Because we teach Git this, commitgraph.readChangedPaths is now redundant, so deprecate it and define its behavior in terms of the config variable we introduce.
This commit does not change the behavior of writing (Git writes changed path filters when explicitly instructed regardless of any config variable), but a subsequent commit will restrict Git such that it will only write when commitgraph.changedPathsVersion is a recognized value.
--- Documentation/config/commitgraph.txt | 26 ++++++++++++++++++++++--- commit-graph.c | 5 +++-- oss-fuzz/fuzz-commit-graph.c | 2 +- repo-settings.c | 6 +++++- repository.h | 2 +- t/t4216-log-bloom.sh | 29 +++++++++++++++++++++++++++- 6 files changed, 61 insertions(+), 9 deletions(-)
Documentation/config/commitgraph.txt +23 -3
@@ -9,6 +9,26 @@ commitGraph.maxNewFilters:: commit-graph write` (c.f., linkgit:git-commit-graph[1]). commitGraph.readChangedPaths:: - If true, then git will use the changed-path Bloom filters in the - commit-graph file (if it exists, and they are present). Defaults to - true. See linkgit:git-commit-graph[1] for more information. + Deprecated. Equivalent to commitGraph.changedPathsVersion=-1 if true, and + commitGraph.changedPathsVersion=0 if false. (If commitGraph.changedPathVersion + is also set, commitGraph.changedPathsVersion takes precedence.) + +commitGraph.changedPathsVersion:: + Specifies the version of the changed-path Bloom filters that Git will read and + write. May be -1, 0 or 1. Note that values greater than 1 may be + incompatible with older versions of Git which do not yet understand + those versions. Use caution when operating in a mixed-version + environment. ++ +Defaults to -1. ++ +If -1, Git will use the version of the changed-path Bloom filters in the +repository, defaulting to 1 if there are none. ++ +If 0, Git will not read any Bloom filters, and will write version 1 Bloom +filters when instructed to write. ++ +If 1, Git will only read version 1 Bloom filters, and will write version 1 +Bloom filters. ++ +See linkgit:git-commit-graph[1] for more information.
commit-graph.c +3 -2
@@ -459,7 +459,7 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, graph->read_generation_data =1 ; } -if (s->commit_graph_read_changed_paths) { +if (s->commit_graph_changed_paths_version) {read_chunk (cf,GRAPH_CHUNKID_BLOOMINDEXES , graph_read_bloom_index, graph); read_chunk(cf, GRAPH_CHUNKID_BLOOMDATA,
@@ -555,7 +555,8 @@ static void validate_mixed_bloom_settings(struct commit_graph *g) }if (g->bloom_filter_settings->bits_per_entry != settings->bits_per_entry || - g->bloom_filter_settings->num_hashes != settings->num_hashes) { + g->bloom_filter_settings->num_hashes != settings->num_hashes || + g->bloom_filter_settings->hash_version != settings->hash_version) { g->chunk_bloom_indexes =NULL ; g->chunk_bloom_data =NULL ;FREE_AND_NULL (g->bloom_filter_settings);
oss-fuzz/fuzz-commit-graph.c +1 -1
@@ -19,7 +19,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)* possible. */ the_repository->settings.commit_graph_generation_version =2 ; - the_repository->settings.commit_graph_read_changed_paths =1 ; + the_repository->settings.commit_graph_changed_paths_version =1 ; g =parse_commit_graph (&the_repository->settings, (void *)data, size);repo_clear (the_repository);free_commit_graph (g);
repo-settings.c +5 -1
@@ -23,6 +23,7 @@ void prepare_repo_settings(struct repository *r)int value;const char *strval;int manyfiles; +int read_changed_paths;if (!r->gitdir)BUG ("Cannot add settings for uninitialized repository" );
@@ -53,7 +54,10 @@ void prepare_repo_settings(struct repository *r)/* Commit graph config or default, does not cascade (simple) */ repo_cfg_bool (r,"core.commitgraph" , &r->settings.core_commit_graph,1 );repo_cfg_int (r,"commitgraph.generationversion" , &r->settings.commit_graph_generation_version,2 ); -repo_cfg_bool (r,"commitgraph.readchangedpaths" , &r->settings.commit_graph_read_changed_paths,1 ); +repo_cfg_bool (r,"commitgraph.readchangedpaths" , &read_changed_paths,1 ); +repo_cfg_int (r,"commitgraph.changedpathsversion" , + &r->settings.commit_graph_changed_paths_version, + read_changed_paths ?-1 :0 );repo_cfg_bool (r,"gc.writecommitgraph" , &r->settings.gc_write_commit_graph,1 );repo_cfg_bool (r,"fetch.writecommitgraph" , &r->settings.fetch_write_commit_graph,0 );
repository.h +1 -1
@@ -29,7 +29,7 @@ struct repo_settings {int core_commit_graph;int commit_graph_generation_version; -int commit_graph_read_changed_paths; +int commit_graph_changed_paths_version;int gc_write_commit_graph;int fetch_write_commit_graph;int command_requires_full_index;
t/t4216-log-bloom.sh +28 -1
@@ -435,7 +435,7 @@ test_expect_success 'setup for mixed Bloom setting tests' ' done ' -test_expect_success 'ensure incompatible Bloom filters are ignored' ' +test_expect_success 'ensure Bloom filters with incompatible settings are ignored' ' # Compute Bloom filters with "unusual" settings. git -C $repo rev-parse one >in && GIT_TEST_BLOOM_SETTINGS_NUM_HASHES=3 git -C $repo commit-graph write \
@@ -485,6 +485,33 @@ test_expect_success 'merge graph layers with incompatible Bloom settings' ' test_must_be_empty err ' +test_expect_success 'ensure Bloom filter with incompatible versions are ignored' ' + rm "$repo/$graph" && + + git -C $repo log --oneline --no-decorate -- $CENT >expect && + + # Compute v1 Bloom filters for commits at the bottom. + git -C $repo rev-parse HEAD^ >in && + git -C $repo commit-graph write --stdin-commits --changed-paths \ + --split <in && + + # Compute v2 Bloomfilters for the rest of the commits at the top. + git -C $repo rev-parse HEAD >in && + git -C $repo -c commitGraph.changedPathsVersion=2 commit-graph write \ + --stdin-commits --changed-paths --split=no-merge <in && + + test_line_count = 2 $repo/$chain && + + git -C $repo log --oneline --no-decorate -- $CENT >actual 2>err && + test_cmp expect actual && + + layer="$(head -n 1 $repo/$chain)" && + cat >expect.err <<-EOF && + warning: disabling Bloom filters for commit-graph layer $SQ$layer$SQ due to incompatible settings + EOF + test_cmp expect.err err +' + get_first_changed_path_filter () { test-tool read-graph bloom-filters >filters.dat && head -n 1 filters.dat
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
Re: [PATCH v5 09/17] repo-settings: introduce commitgraph.changedPathsVersion
Quoted 167 lines · Taylor Blau
A subsequent commit will introduce another version of the changed-path filter in the commit graph file. In order to control which version to write (and read), a config variable is needed.
Therefore, introduce this config variable. For forwards compatibility, teach Git to not read commit graphs when the config variable is set to an unsupported version. Because we teach Git this, commitgraph.readChangedPaths is now redundant, so deprecate it and define its behavior in terms of the config variable we introduce.
This commit does not change the behavior of writing (Git writes changed path filters when explicitly instructed regardless of any config variable), but a subsequent commit will restrict Git such that it will only write when commitgraph.changedPathsVersion is a recognized value.
Signed-off-by Jonathan Tan <[email protected]>Signed-off-by Junio C Hamano <[email protected]>Signed-off-by Taylor Blau <[email protected]>--- Documentation/config/commitgraph.txt | 26 ++++++++++++++++++++++--- commit-graph.c | 5 +++-- oss-fuzz/fuzz-commit-graph.c | 2 +- repo-settings.c | 6 +++++- repository.h | 2 +- t/t4216-log-bloom.sh | 29 +++++++++++++++++++++++++++- 6 files changed, 61 insertions(+), 9 deletions(-)Documentation/config/commitgraph.txt +23
-3@@ -9,6 +9,26 @@ commitGraph.maxNewFilters:: commit-graph write` (c.f., linkgit:git-commit-graph[1]). commitGraph.readChangedPaths:: - If true, then git will use the changed-path Bloom filters in the - commit-graph file (if it exists, and they are present). Defaults to - true. See linkgit:git-commit-graph[1] for more information. + Deprecated. Equivalent to commitGraph.changedPathsVersion=-1 if true, and + commitGraph.changedPathsVersion=0 if false. (If commitGraph.changedPathVersion + is also set, commitGraph.changedPathsVersion takes precedence.) + +commitGraph.changedPathsVersion:: + Specifies the version of the changed-path Bloom filters that Git will read and + write. May be -1, 0 or 1. Note that values greater than 1 may be + incompatible with older versions of Git which do not yet understand + those versions. Use caution when operating in a mixed-version + environment. ++ +Defaults to -1. ++ +If -1, Git will use the version of the changed-path Bloom filters in the +repository, defaulting to 1 if there are none. ++ +If 0, Git will not read any Bloom filters, and will write version 1 Bloom +filters when instructed to write. ++ +If 1, Git will only read version 1 Bloom filters, and will write version 1 +Bloom filters. ++ +See linkgit:git-commit-graph[1] for more information.commit-graph.c +3
-2@@ -459,7 +459,7 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, graph->read_generation_data =1 ; } -if (s->commit_graph_read_changed_paths) { +if (s->commit_graph_changed_paths_version) {read_chunk (cf,GRAPH_CHUNKID_BLOOMINDEXES , graph_read_bloom_index, graph); read_chunk(cf, GRAPH_CHUNKID_BLOOMDATA,@@ -555,7 +555,8 @@ static void validate_mixed_bloom_settings(struct commit_graph *g) }if (g->bloom_filter_settings->bits_per_entry != settings->bits_per_entry || - g->bloom_filter_settings->num_hashes != settings->num_hashes) { + g->bloom_filter_settings->num_hashes != settings->num_hashes || + g->bloom_filter_settings->hash_version != settings->hash_version) { g->chunk_bloom_indexes =NULL ; g->chunk_bloom_data =NULL ;FREE_AND_NULL (g->bloom_filter_settings);oss-fuzz/fuzz-commit-graph.c +1
-1@@ -19,7 +19,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)* possible. */ the_repository->settings.commit_graph_generation_version =2 ; - the_repository->settings.commit_graph_read_changed_paths =1 ; + the_repository->settings.commit_graph_changed_paths_version =1 ; g =parse_commit_graph (&the_repository->settings, (void *)data, size);repo_clear (the_repository);free_commit_graph (g);repo-settings.c +5
-1@@ -23,6 +23,7 @@ void prepare_repo_settings(struct repository *r)int value;const char *strval;int manyfiles; +int read_changed_paths;if (!r->gitdir)BUG ("Cannot add settings for uninitialized repository" );@@ -53,7 +54,10 @@ void prepare_repo_settings(struct repository *r)/* Commit graph config or default, does not cascade (simple) */ repo_cfg_bool (r,"core.commitgraph" , &r->settings.core_commit_graph,1 );repo_cfg_int (r,"commitgraph.generationversion" , &r->settings.commit_graph_generation_version,2 ); -repo_cfg_bool (r,"commitgraph.readchangedpaths" , &r->settings.commit_graph_read_changed_paths,1 ); +repo_cfg_bool (r,"commitgraph.readchangedpaths" , &read_changed_paths,1 ); +repo_cfg_int (r,"commitgraph.changedpathsversion" , + &r->settings.commit_graph_changed_paths_version, + read_changed_paths ?-1 :0 );repo_cfg_bool (r,"gc.writecommitgraph" , &r->settings.gc_write_commit_graph,1 );repo_cfg_bool (r,"fetch.writecommitgraph" , &r->settings.fetch_write_commit_graph,0 );repository.h +1
-1@@ -29,7 +29,7 @@ struct repo_settings {int core_commit_graph;int commit_graph_generation_version; -int commit_graph_read_changed_paths; +int commit_graph_changed_paths_version;int gc_write_commit_graph;int fetch_write_commit_graph;int command_requires_full_index;t/t4216-log-bloom.sh +17
-1@@ -435,7 +435,7 @@ test_expect_success 'setup for mixed Bloom setting tests' ' done ' -test_expect_success 'ensure incompatible Bloom filters are ignored' ' +test_expect_success 'ensure Bloom filters with incompatible settings are ignored' ' # Compute Bloom filters with "unusual" settings. git -C $repo rev-parse one >in && GIT_TEST_BLOOM_SETTINGS_NUM_HASHES=3 git -C $repo commit-graph write \@@ -485,6 +485,33 @@ test_expect_success 'merge graph layers with incompatible Bloom settings' ' test_must_be_empty err ' +test_expect_success 'ensure Bloom filter with incompatible versions are ignored' ' + rm "$repo/$graph" && + + git -C $repo log --oneline --no-decorate -- $CENT >expect && + + # Compute v1 Bloom filters for commits at the bottom. + git -C $repo rev-parse HEAD^ >in && + git -C $repo commit-graph write --stdin-commits --changed-paths \ + --split <in && + + # Compute v2 Bloomfilters for the rest of the commits at the top. + git -C $repo rev-parse HEAD >in && + git -C $repo -c commitGraph.changedPathsVersion=2 commit-graph write \ + --stdin-commits --changed-paths --split=no-merge <in && + + test_line_count = 2 $repo/$chain &&
t/t4216-log-bloom.sh test_expect_success 'merge graph layers with incompatible Bloom settings' ' · line 513
Quoted 10 lines
t/t4216-log-bloom.sh +10
-0+ + git -C $repo log --oneline --no-decorate -- $CENT >actual 2>err && + test_cmp expect actual && + + layer="$(head -n 1 $repo/$chain)" && + cat >expect.err <<-EOF && + warning: disabling Bloom filters for commit-graph layer $SQ$layer$SQ due to incompatible settings + EOF + test_cmp expect.err err +'
At this point in the series this test fails with:
+ test_cmp expect.err err
+ test 2 -ne 2
+ eval diff -u "$@"
+ diff -u expect.err err
--- expect.err 2024-01-29 21:02:57.927462620 +0000
+++ err 2024-01-29 21:02:57.923462642 +0000
@@ -1 +0,0 @@
-warning: disabling Bloom filters for commit-graph layer 'e338a7a1b4cfa5f6bcd31aea3e027df67d06442a' due to incompatible settings
error: last command exited with $?=1
Quoted 7 lines
t/t4216-log-bloom.sh +1
-0+ get_first_changed_path_filter () { test-tool read-graph bloom-filters >filters.dat && head -n 1 filters.datSignature · 1 line
2.43.0.334.gd4dbce1db5.dirty
Re: [PATCH v5 09/17] repo-settings: introduce commitgraph.changedPathsVersion
Quoted 11 lines · SZEDER Gábor
At this point in the series this test fails with:
+
test_cmpexpect.err err
+ test 2 -ne 2
+ eval diff -u "$@"
+ diff -u expect.err err
--- expect.err 2024-01-29 21:02:57.927462620 +0000
+++ err 2024-01-29 21:02:57.923462642 +0000
@@ -1 +0,0 @@
-warning: disabling Bloom filters for commit-graph layer 'e338a7a1b4cfa5f6bcd31aea3e027df67d06442a' due to incompatible settings
error: last command exited with $?=1
Very good catch, thanks, I'm not sure how this one slipped through.
The fix should be mostly trivial, but I'll have to reroll the series since it has some minor fallout outside of just this patch.
Junio, please hold off on merging this to 'next' until I've had a chance to send out a new round.
Thanks,
Taylor
PATCH v5 10/17 commit-graph: new Bloom filter version that fixes murmur3
In reply to Taylor BlauThe murmur3 implementation in bloom.c has a bug when converting series of 4 bytes into network-order integers when char is signed (which is controllable by a compiler option, and the default signedness of char is platform-specific). When a string contains characters with the high bit set, this bug causes results that, although internally consistent within Git, does not accord with other implementations of murmur3 (thus, the changed path filters wouldn't be readable by other off-the-shelf implementatios of murmur3) and even with Git binaries that were compiled with different signedness of char. This bug affects both how Git writes changed path filters to disk and how Git interprets changed path filters on disk.
Therefore, introduce a new version (2) of changed path filters that corrects this problem. The existing version (1) is still supported and is still the default, but users should migrate away from it as soon as possible.
Because this bug only manifests with characters that have the high bit set, it may be possible that some (or all) commits in a given repo would have the same changed path filter both before and after this fix is applied. However, in order to determine whether this is the case, the changed paths would first have to be computed, at which point it is not much more expensive to just compute a new changed path filter.
So this patch does not include any mechanism to "salvage" changed path filters from repositories. There is also no "mixed" mode - for each invocation of Git, reading and writing changed path filters are done with the same version number; this version number may be explicitly stated (typically if the user knows which version they need) or automatically determined from the version of the existing changed path filters in the repository.
There is a change in write_commit_graph(). graph_read_bloom_data() makes it possible for chunk_bloom_data to be non-NULL but bloom_filter_settings to be NULL, which causes a segfault later on. I produced such a segfault while developing this patch, but couldn't find a way to reproduce it neither after this complete patch (or before), but in any case it seemed like a good thing to include that might help future patch authors.
The value in t0095 was obtained from another murmur3 implementation using the following Go source code:
package main
import "fmt"
import "github.com/spaolacci/murmur3"
func main() {
fmt.Printf("%x\n", murmur3.Sum32([]byte("Hello world!")))
fmt.Printf("%x\n", murmur3.Sum32([]byte{0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}))
}--- Documentation/config/commitgraph.txt | 5 +- bloom.c | 69 +++++++++++++++- bloom.h | 8 +- commit-graph.c | 37 +++++++-- t/helper/test-bloom.c | 9 ++- t/t0095-bloom.sh | 8 ++ t/t4216-log-bloom.sh | 114 +++++++++++++++++++++++++++ 7 files changed, 234 insertions(+), 16 deletions(-)
Documentation/config/commitgraph.txt +4 -1
@@ -15,7 +15,7 @@ commitGraph.readChangedPaths:: commitGraph.changedPathsVersion:: Specifies the version of the changed-path Bloom filters that Git will read and - write. May be -1, 0 or 1. Note that values greater than 1 may be + write. May be -1, 0, 1, or 2. Note that values greater than 1 may be incompatible with older versions of Git which do not yet understand those versions. Use caution when operating in a mixed-version environment.
@@ -31,4 +31,7 @@ filters when instructed to write. If 1, Git will only read version 1 Bloom filters, and will write version 1 Bloom filters. + +If 2, Git will only read version 2 Bloom filters, and will write version 2 +Bloom filters. ++ See linkgit:git-commit-graph[1] for more information.
bloom.c +66 -3
81 lines not on this page · raw message
bloom.h +5 -3
@@ -8,9 +8,11 @@ struct commit_graph;struct bloom_filter_settings {/* * The version of the hashing technique being used. -* We currently only support version = 1 which is +* The newest version is 2, which is * the seeded murmur3 hashing technique implemented -* in bloom.c. +* in bloom.c. Bloom filters of version 1 were created +* with prior versions of Git, which had a bug in the +* implementation of the hash function. */ uint32_t hash_version;
@@ -80,7 +82,7 @@ int load_bloom_filter_from_graph(struct commit_graph *g,* Not considered to be cryptographically secure. * Implemented as described in https://en.wikipedia.org/wiki/MurmurHash#Algorithm */ -uint32_t murmur3_seeded (uint32_t seed,const char *data,size_t len); +uint32_t murmur3_seeded_v2 (uint32_t seed,const char *data,size_t len); void fill_bloom_key(const char *data, size_t len,
commit-graph.c +29 -8
@@ -340,10 +340,16 @@ static int graph_read_bloom_index(const unsigned char *chunk_start,return 0 ; } +struct graph_read_bloom_data_context { +struct commit_graph *g; +int *commit_graph_changed_paths_version; +}; +static int graph_read_bloom_data (const unsigned char *chunk_start,size_t chunk_size,void *data) { -struct commit_graph *g = data; +struct graph_read_bloom_data_context *c = data; +struct commit_graph *g = c->g;uint32_t hash_version;if (chunk_size <BLOOMDATA_CHUNK_HEADER_SIZE ) {
@@ -354,13 +360,15 @@ static int graph_read_bloom_data(const unsigned char *chunk_start,return -1 ; } + hash_version =get_be32 (chunk_start); + +if (*c->commit_graph_changed_paths_version ==-1 ) + *c->commit_graph_changed_paths_version = hash_version; +else if (hash_version != *c->commit_graph_changed_paths_version) +return 0 ; + g->chunk_bloom_data = chunk_start; g->chunk_bloom_data_size = chunk_size; - hash_version =get_be32 (chunk_start); - -if (hash_version !=1 ) -return 0 ; - g->bloom_filter_settings =xmalloc (sizeof (struct bloom_filter_settings )); g->bloom_filter_settings->hash_version = hash_version; g->bloom_filter_settings->num_hashes =get_be32 (chunk_start +4 );
@@ -460,10 +468,14 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, }if (s->commit_graph_changed_paths_version) { +struct graph_read_bloom_data_context context = { + .g = graph, + .commit_graph_changed_paths_version = &s->commit_graph_changed_paths_version + };read_chunk (cf,GRAPH_CHUNKID_BLOOMINDEXES , graph_read_bloom_index, graph);read_chunk (cf,GRAPH_CHUNKID_BLOOMDATA , - graph_read_bloom_data, graph); + graph_read_bloom_data, &context); }if (graph->chunk_bloom_indexes && graph->chunk_bloom_data) {
@@ -2501,6 +2513,13 @@ int write_commit_graph(struct object_directory *odb, }if (!commit_graph_compatible (r))return 0 ; +if (r->settings.commit_graph_changed_paths_version <-1 + || r->settings.commit_graph_changed_paths_version >2 ) { +warning (_ ("attempting to write a commit-graph, but " +"'commitgraph.changedPathsVersion' (%d) is not supported" ), + r->settings.commit_graph_changed_paths_version); +return 0 ; + }CALLOC_ARRAY (ctx,1 ); ctx->r = r;
@@ -2513,6 +2532,8 @@ int write_commit_graph(struct object_directory *odb, ctx->write_generation_data = (get_configured_generation_version (r) ==2 ); ctx->num_generation_data_overflows =0 ; + bloom_settings.hash_version = r->settings.commit_graph_changed_paths_version ==2 + ?2 :1 ; bloom_settings.bits_per_entry =git_env_ulong ("GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY" , bloom_settings.bits_per_entry); bloom_settings.num_hashes = git_env_ulong("GIT_TEST_BLOOM_SETTINGS_NUM_HASHES",
@@ -2542,7 +2563,7 @@ int write_commit_graph(struct object_directory *odb, g = ctx->r->objects->commit_graph;/* We have changed-paths already. Keep them in the next graph */ -if (g && g->chunk_bloom_data) { +if (g && g->bloom_filter_settings) { ctx->changed_paths =1 ; ctx->bloom_settings = g->bloom_filter_settings; }
t/helper/test-bloom.c +8 -1
@@ -49,6 +49,7 @@ static void get_bloom_filter_for_commit(const struct object_id *commit_oid)static const char *bloom_usage ="\n" " test-tool bloom get_murmur3 <string>\n" +" test-tool bloom get_murmur3_seven_highbit\n" " test-tool bloom generate_filter <string> [<string>...]\n" " test-tool bloom get_filter_for_commit <commit-hex>\n" ;
@@ -63,7 +64,13 @@ int cmd__bloom(int argc, const char **argv)uint32_t hashed;if (argc <3 )usage (bloom_usage); - hashed =murmur3_seeded (0 , argv[2 ],strlen (argv[2 ])); + hashed =murmur3_seeded_v2 (0 , argv[2 ],strlen (argv[2 ])); +printf ("Murmur3 Hash with seed=0:0x%08x\n" , hashed); + } + +if (!strcmp (argv[1 ],"get_murmur3_seven_highbit" )) { +uint32_t hashed; + hashed =murmur3_seeded_v2 (0 ,"\x99\xaa\xbb\xcc\xdd\xee\xff" ,7 );printf ("Murmur3 Hash with seed=0:0x%08x\n" , hashed); }
t/t0095-bloom.sh +8 -0
@@ -29,6 +29,14 @@ test_expect_success 'compute unseeded murmur3 hash for test string 2' ' test_cmp expect actual ' +test_expect_success 'compute unseeded murmur3 hash for test string 3' ' + cat >expect <<-\EOF && + Murmur3 Hash with seed=0:0xa183ccfd + EOF + test-tool bloom get_murmur3_seven_highbit >actual && + test_cmp expect actual +' + test_expect_success 'compute bloom key for empty string' ' cat >expect <<-\EOF && Hashes:0x5615800c|0x5b966560|0x61174ab4|0x66983008|0x6c19155c|0x7199fab0|0x771ae004|
t/t4216-log-bloom.sh +114 -0
120 lines not on this page · raw message
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 11/17 bloom: annotate filters with hash version
In reply to Taylor BlauIn subsequent commits, we will want to load existing Bloom filters out of a commit-graph, even when the hash version they were computed with does not match the value of commitGraph.changedPathVersion.
In order to differentiate between the two, add a "version" field to each Bloom filter.
--- bloom.c | 11 ++++++++--- bloom.h | 1 + 2 files changed, 9 insertions(+), 3 deletions(-)
bloom.c +8 -3
@@ -88,6 +88,7 @@ int load_bloom_filter_from_graph(struct commit_graph *g, filter->data = (unsigned char *)(g->chunk_bloom_data +sizeof (unsigned char ) * start_index +BLOOMDATA_CHUNK_HEADER_SIZE ); + filter->version = g->bloom_filter_settings->hash_version;return 1 ; }
@@ -273,11 +274,13 @@ static int pathmap_cmp(const void *hashmap_cmp_fn_data UNUSED,return strcmp (e1->path, e2->path); } -static void init_truncated_large_filter (struct bloom_filter *filter) +static void init_truncated_large_filter (struct bloom_filter *filter, +int version) { filter->data =xmalloc (1 ); filter->data[0 ] =0xFF ; filter->len =1 ; + filter->version = version; } struct bloom_filter *get_or_compute_bloom_filter(struct repository *r,
@@ -362,13 +365,15 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, }if (hashmap_get_size (&pathmap) > settings->max_changed_paths) { -init_truncated_large_filter (filter); +init_truncated_large_filter (filter, + settings->hash_version);if (computed) *computed |=BLOOM_TRUNC_LARGE ; goto cleanup; } filter->len = (hashmap_get_size (&pathmap) * settings->bits_per_entry +BITS_PER_WORD -1 ) /BITS_PER_WORD ; + filter->version = settings->hash_version;if (!filter->len) {if (computed) *computed |=BLOOM_TRUNC_EMPTY ;
@@ -388,7 +393,7 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, }else {for (i =0 ; i < diff_queued_diff.nr; i++)diff_free_filepair (diff_queued_diff.queue[i]); -init_truncated_large_filter (filter); +init_truncated_large_filter (filter, settings->hash_version);if (computed) *computed |=BLOOM_TRUNC_LARGE ;
bloom.h +1 -0
@@ -55,6 +55,7 @@ struct bloom_filter_settings {struct bloom_filter {unsigned char *data;size_t len; +int version; };/*
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 12/17 bloom: prepare to discard incompatible Bloom filters
In reply to Taylor BlauCallers use the inline get_bloom_filter() implementation as a thin wrapper around get_or_compute_bloom_filter(). The former calls the latter with a value of "0" for compute_if_not_present, making get_bloom_filter() the default read-only path for fetching an existing Bloom filter.
Callers expect the value returned from get_bloom_filter() is usable, that is that it's compatible with the configured value corresponding to commitGraph.changedPathsVersion.
This is OK, since the commit-graph machinery only initializes its BDAT chunk (thereby enabling it to service Bloom filter queries) when the Bloom filter hash_version is compatible with our settings. So any value returned by get_bloom_filter() is trivially useable.
However, subsequent commits will load the BDAT chunk even when the Bloom filters are built with incompatible hash versions. Prepare to handle this by teaching get_bloom_filter() to discard filters that are incompatible with the configured hash version.
Callers who wish to read incompatible filters (e.g., for upgrading filters from v1 to v2) may use the lower level routine, get_or_compute_bloom_filter().
--- bloom.c | 20 +++++++++++++++++++- bloom.h | 20 ++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-)
bloom.c +19 -1
@@ -283,6 +283,23 @@ static void init_truncated_large_filter(struct bloom_filter *filter, filter->version = version; } +struct bloom_filter *get_bloom_filter (struct repository *r,struct commit *c) +{ +struct bloom_filter *filter; +int hash_version; + + filter =get_or_compute_bloom_filter (r, c,0 ,NULL ,NULL ); +if (!filter) +return NULL ; + +prepare_repo_settings (r); + hash_version = r->settings.commit_graph_changed_paths_version; + +if (!(hash_version ==-1 || hash_version == filter->version)) +return NULL ;/* unusable filter */ +return filter; +} + struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, struct commit *c, int compute_if_not_present,
@@ -308,7 +325,8 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, filter, graph_pos); } -if (filter->data && filter->len) +if ((filter->data && filter->len) && + (!settings || settings->hash_version == filter->version))return filter;if (!compute_if_not_present)return NULL ;
bloom.h +18 -2
@@ -110,8 +110,24 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r,const struct bloom_filter_settings *settings, enum bloom_filter_computed *computed); -#define get_bloom_filter (r, c) get_or_compute_bloom_filter( \ - (r), (c), 0, NULL, NULL) +/* +* Find the Bloom filter associated with the given commit "c". +* +* If any of the following are true +* +* - the repository does not have a commit-graph, or +* - the repository disables reading from the commit-graph, or +* - the given commit does not have a Bloom filter computed, or +* - there is a Bloom filter for commit "c", but it cannot be read +* because the filter uses an incompatible version of murmur3 +* +* , then `get_bloom_filter()` will return NULL. Otherwise, the corresponding +* Bloom filter will be returned. +* +* For callers who wish to inspect Bloom filters with incompatible hash +* versions, use get_or_compute_bloom_filter(). +*/ +struct bloom_filter *get_bloom_filter (struct repository *r,struct commit *c); int bloom_filter_contains(const struct bloom_filter *filter, const struct bloom_key *key,
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 13/17 commit-graph.c: unconditionally load Bloom filters
In reply to Taylor BlauIn an earlier commit, we began ignoring the Bloom data ("BDAT") chunk for commit-graphs whose Bloom filters were computed using a hash version
incompatible with the value of commitGraph.changedPathVersion.
Now that the Bloom API has been hardened to discard these incompatible filters (with the exception of low-level APIs), we can safely load these Bloom filters unconditionally.
We no longer want to return early from graph_read_bloom_data(), and similarly do not want to set the bloom_settings' hash_version field as a side-effect. The latter is because we want to wait until we know which Bloom settings we're using (either the defaults, from the GIT_TEST variables, or from the previous commit-graph layer) before deciding what hash_version to use.
If we detect an existing BDAT chunk, we'll infer the rest of the settings (e.g., number of hashes, bits per entry, and maximum number of changed paths) from the earlier graph layer. The hash_version will be inferred from the previous layer as well, unless one has already been specified via configuration.
Once all of that is done, we normalize the value of the hash_version to either "1" or "2".
--- commit-graph.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-)
commit-graph.c +10 -8
@@ -362,11 +362,6 @@ static int graph_read_bloom_data(const unsigned char *chunk_start, hash_version =get_be32 (chunk_start); -if (*c->commit_graph_changed_paths_version ==-1 ) - *c->commit_graph_changed_paths_version = hash_version; -else if (hash_version != *c->commit_graph_changed_paths_version) -return 0 ; - g->chunk_bloom_data = chunk_start; g->chunk_bloom_data_size = chunk_size; g->bloom_filter_settings =xmalloc (sizeof (struct bloom_filter_settings ));
@@ -2532,8 +2527,7 @@ int write_commit_graph(struct object_directory *odb, ctx->write_generation_data = (get_configured_generation_version (r) ==2 ); ctx->num_generation_data_overflows =0 ; - bloom_settings.hash_version = r->settings.commit_graph_changed_paths_version ==2 - ?2 :1 ; + bloom_settings.hash_version = r->settings.commit_graph_changed_paths_version; bloom_settings.bits_per_entry =git_env_ulong ("GIT_TEST_BLOOM_SETTINGS_BITS_PER_ENTRY" , bloom_settings.bits_per_entry); bloom_settings.num_hashes = git_env_ulong("GIT_TEST_BLOOM_SETTINGS_NUM_HASHES",
@@ -2565,10 +2559,18 @@ int write_commit_graph(struct object_directory *odb,/* We have changed-paths already. Keep them in the next graph */ if (g && g->bloom_filter_settings) { ctx->changed_paths =1 ; - ctx->bloom_settings = g->bloom_filter_settings; + +/* don't propagate the hash_version unless unspecified */ +if (bloom_settings.hash_version ==-1 ) + bloom_settings.hash_version = g->bloom_filter_settings->hash_version; + bloom_settings.bits_per_entry = g->bloom_filter_settings->bits_per_entry; + bloom_settings.num_hashes = g->bloom_filter_settings->num_hashes; + bloom_settings.max_changed_paths = g->bloom_filter_settings->max_changed_paths; } } + bloom_settings.hash_version = bloom_settings.hash_version ==2 ?2 :1 ; +if (ctx->split) {struct commit_graph *g = ctx->r->objects->commit_graph;
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 14/17 commit-graph: drop unnecessary `graph_read_bloom_data_context`
In reply to Taylor BlauThe graph_read_bloom_data_context struct was introduced in an earlier commit in order to pass pointers to the commit-graph and changed-path Bloom filter version when reading the BDAT chunk.
The previous commit no longer writes through the changed_paths_version pointer, making the surrounding context structure unnecessary. Drop it and pass a pointer to the commit-graph directly when reading the BDAT chunk.
--- commit-graph.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-)
commit-graph.c +2 -12
@@ -340,16 +340,10 @@ static int graph_read_bloom_index(const unsigned char *chunk_start,return 0 ; } -struct graph_read_bloom_data_context { -struct commit_graph *g; -int *commit_graph_changed_paths_version; -}; -static int graph_read_bloom_data (const unsigned char *chunk_start,size_t chunk_size,void *data) { -struct graph_read_bloom_data_context *c = data; -struct commit_graph *g = c->g; +struct commit_graph *g = data;uint32_t hash_version;if (chunk_size <BLOOMDATA_CHUNK_HEADER_SIZE ) {
@@ -463,14 +457,10 @@ struct commit_graph *parse_commit_graph(struct repo_settings *s, }if (s->commit_graph_changed_paths_version) { -struct graph_read_bloom_data_context context = { - .g = graph, - .commit_graph_changed_paths_version = &s->commit_graph_changed_paths_version - };read_chunk (cf,GRAPH_CHUNKID_BLOOMINDEXES , graph_read_bloom_index, graph);read_chunk (cf,GRAPH_CHUNKID_BLOOMDATA , - graph_read_bloom_data, &context); + graph_read_bloom_data, graph); }if (graph->chunk_bloom_indexes && graph->chunk_bloom_data) {
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 15/17 object.h: fix mis-aligned flag bits table
In reply to Taylor BlauBit position 23 is one column too far to the left.
--- object.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
object.h +1 -1
@@ -62,7 +62,7 @@ void object_array_init(struct object_array *array);/* * object flag allocation: -* revision.h: 0---------10 15 23------27 +* revision.h: 0---------10 15 23------27 * fetch-pack.c: 01 67 * negotiator/default.c: 2--5 * walker.c: 0-2
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 16/17 commit-graph: reuse existing Bloom filters where possible
In reply to Taylor BlauIn an earlier commit, a bug was described where it's possible for Git to produce non-murmur3 hashes when the platform's "char" type is signed, and there are paths with characters whose highest bit is set (i.e. all characters >= 0x80).
That patch allows the caller to control which version of Bloom filters are read and written. However, even on platforms with a signed "char" type, it is possible to reuse existing Bloom filters if and only if there are no changed paths in any commit's first parent tree-diff whose characters have their highest bit set.
When this is the case, we can reuse the existing filter without having to compute a new one. This is done by marking trees which are known to have (or not have) any such paths. When a commit's root tree is verified to not have any such paths, we mark it as such and declare that the commit's Bloom filter is reusable.
Note that this heuristic only goes in one direction. If neither a commit nor its first parent have any paths in their trees with non-ASCII characters, then we know for certain that a path with non-ASCII characters will not appear in a tree-diff against that commit's first parent. The reverse isn't necessarily true: just because the tree-diff doesn't contain any such paths does not imply that no such paths exist in either tree.
So we end up recomputing some Bloom filters that we don't strictly have to (i.e. their bits are the same no matter which version of murmur3 we use). But culling these out is impossible, since we'd have to perform the full tree-diff, which is the same effort as computing the Bloom filter from scratch.
But because we can cache our results in each tree's flag bits, we can often avoid recomputing many filters, thereby reducing the time it takes to run
$ git commit-graph write --changed-paths --reachable
when upgrading from v1 to v2 Bloom filters.
To benchmark this, let's generate a commit-graph in linux.git with v1 changed-paths in generation order[^1]:
$ git clone [email protected]:torvalds/linux.git $ cd linux $ git commit-graph write --reachable --changed-paths $ graph=".git/objects/info/commit-graph" $ mv $graph{,.bak}
Then let's time how long it takes to go from v1 to v2 filters (with and without the upgrade path enabled), resetting the state of the commit-graph each time:
$ git config commitGraph.changedPathsVersion 2
$ hyperfine -p 'cp -f $graph.bak $graph' -L v 0,1 \
'GIT_TEST_UPGRADE_BLOOM_FILTERS={v} git.compile commit-graph write --reachable --changed-paths'On linux.git (where there aren't any non-ASCII paths), the timings indicate that this patch represents a speed-up over recomputing all Bloom filters from scratch:
Benchmark 1: GIT_TEST_UPGRADE_BLOOM_FILTERS=0 git.compile commit-graph write --reachable --changed-paths
Time (mean ± σ): 124.873 s ± 0.316 s [User: 124.081 s, System: 0.643 s]
Range (min … max): 124.621 s … 125.227 s 3 runs
Benchmark 2: GIT_TEST_UPGRADE_BLOOM_FILTERS=1 git.compile commit-graph write --reachable --changed-paths
Time (mean ± σ): 79.271 s ± 0.163 s [User: 74.611 s, System: 4.521 s]
Range (min … max): 79.112 s … 79.437 s 3 runs
Summary
'GIT_TEST_UPGRADE_BLOOM_FILTERS=1 git.compile commit-graph write --reachable --changed-paths' ran
1.58 ± 0.01 times faster than 'GIT_TEST_UPGRADE_BLOOM_FILTERS=0 git.compile commit-graph write --reachable --changed-paths'On git.git, we do have some non-ASCII paths, giving us a more modest improvement from 4.163 seconds to 3.348 seconds, for a 1.24x speed-up. On my machine, the stats for git.git are:
- 8,285 Bloom filters computed from scratch
- 10 Bloom filters generated as empty
- 4 Bloom filters generated as truncated due to too many changed paths
- 65,114 Bloom filters were reused when transitioning from v1 to v2.
[^1]: Note that this is is important, since --stdin-packs or
--stdin-commits orders commits in the commit-graph by their pack position (with --stdin-packs) or in the raw input (with --stdin-commits).
Since we compute Bloom filters in the same order that commits appear in the graph, we must see a commit's (first) parent before we process the commit itself. This is only guaranteed to happen when sorting commits by their generation number.
--- bloom.c | 90 ++++++++++++++++++++++++++++++++++++++++++-- bloom.h | 1 + commit-graph.c | 5 +++ object.h | 1 + t/t4216-log-bloom.sh | 35 ++++++++++++++++- 5 files changed, 128 insertions(+), 4 deletions(-)
bloom.c +87 -3
@@ -6,6 +6,9 @@#include "commit-graph.h" #include "commit.h" #include "commit-slab.h" +#include "tree.h" +#include "tree-walk.h" +#include "config.h" define_commit_slab(bloom_filter_slab, struct bloom_filter);
@@ -283,6 +286,73 @@ static void init_truncated_large_filter(struct bloom_filter *filter, filter->version = version; } +#define VISITED (1u<<21) +#define HIGH_BITS (1u<<22) + +static int has_entries_with_high_bit (struct repository *r,struct tree *t) +{ +if (parse_tree (t)) +return 1 ; + +if (!(t->object.flags &VISITED )) { +struct tree_desc desc; +struct name_entry entry; + +init_tree_desc (&desc, t->buffer, t->size); +while (tree_entry (&desc, &entry)) { +size_t i; +for (i =0 ; i < entry.pathlen; i++) { +if (entry.path[i] &0x80 ) { + t->object.flags |=HIGH_BITS ; + goto done; + } + } + +if (S_ISDIR (entry.mode)) { +struct tree *sub =lookup_tree (r, &entry.oid); +if (sub &&has_entries_with_high_bit (r, sub)) { + t->object.flags |=HIGH_BITS ; + goto done; + } + } + + } + +done: + t->object.flags |=VISITED ; + } + +return !!(t->object.flags &HIGH_BITS ); +} + +static int commit_tree_has_high_bit_paths (struct repository *r, +struct commit *c) +{ +struct tree *t; +if (repo_parse_commit (r, c)) +return 1 ; + t =repo_get_commit_tree (r, c); +if (!t) +return 1 ; +return has_entries_with_high_bit (r, t); +} + +static struct bloom_filter *upgrade_filter (struct repository *r,struct commit *c, +struct bloom_filter *filter, +int hash_version) +{ +struct commit_list *p = c->parents; +if (commit_tree_has_high_bit_paths (r, c)) +return NULL ; + +if (p &&commit_tree_has_high_bit_paths (r, p->item)) +return NULL ; + + filter->version = hash_version; + +return filter; +} +struct bloom_filter *get_bloom_filter (struct repository *r,struct commit *c) {struct bloom_filter *filter;
@@ -325,9 +395,23 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, filter, graph_pos); } -if ((filter->data && filter->len) && - (!settings || settings->hash_version == filter->version)) -return filter; +if (filter->data && filter->len) { +struct bloom_filter *upgrade; +if (!settings || settings->hash_version == filter->version) +return filter; + +/* version mismatch, see if we can upgrade */ +if (compute_if_not_present && +git_env_bool ("GIT_TEST_UPGRADE_BLOOM_FILTERS" ,1 )) { + upgrade =upgrade_filter (r, c, filter, + settings->hash_version); +if (upgrade) { +if (computed) + *computed |=BLOOM_UPGRADED ; +return upgrade; + } + } + }if (!compute_if_not_present)return NULL ;
bloom.h +1 -0
@@ -102,6 +102,7 @@ enum bloom_filter_computed {BLOOM_COMPUTED = (1 <<1 ),BLOOM_TRUNC_LARGE = (1 <<2 ),BLOOM_TRUNC_EMPTY = (1 <<3 ), + BLOOM_UPGRADED = (1 << 4), }; struct bloom_filter *get_or_compute_bloom_filter(struct repository *r,
commit-graph.c +5 -0
@@ -1167,6 +1167,7 @@ struct write_commit_graph_context {int count_bloom_filter_not_computed;int count_bloom_filter_trunc_empty;int count_bloom_filter_trunc_large; +int count_bloom_filter_upgraded; }; static int write_graph_chunk_fanout(struct hashfile *f,
@@ -1774,6 +1775,8 @@ static void trace2_bloom_filter_write_statistics(struct write_commit_graph_conte ctx->count_bloom_filter_trunc_empty);trace2_data_intmax ("commit-graph" , ctx->r,"filter-trunc-large" , ctx->count_bloom_filter_trunc_large); +trace2_data_intmax ("commit-graph" , ctx->r,"filter-upgraded" , + ctx->count_bloom_filter_upgraded); }static void compute_bloom_filters (struct write_commit_graph_context *ctx)
@@ -1815,6 +1818,8 @@ static void compute_bloom_filters(struct write_commit_graph_context *ctx) ctx->count_bloom_filter_trunc_empty++;if (computed &BLOOM_TRUNC_LARGE ) ctx->count_bloom_filter_trunc_large++; + }else if (computed &BLOOM_UPGRADED ) { + ctx->count_bloom_filter_upgraded++; }else if (computed &BLOOM_NOT_COMPUTED ) ctx->count_bloom_filter_not_computed++; ctx->total_bloom_filter_data_size += filter
object.h +1 -0
@@ -75,6 +75,7 @@ void object_array_init(struct object_array *array);* commit-reach.c: 16-----19 * sha1-name.c: 20 * list-objects-filter.c: 21 +* bloom.c: 2122 * builtin/fsck.c: 0--3 * builtin/gc.c: 0 * builtin/index-pack.c: 2021
t/t4216-log-bloom.sh +34 -1
@@ -222,6 +222,10 @@ test_filter_trunc_large () { grep "\"key\":\"filter-trunc-large\",\"value\":\"$1\"" $2 } +test_filter_upgraded () { + grep "\"key\":\"filter-upgraded\",\"value\":\"$1\"" $2 +} + test_expect_success 'correctly report changes over limit' ' git init limits && (
@@ -656,7 +660,13 @@ test_expect_success 'when writing another commit graph, preserve existing versio test_expect_success 'when writing commit graph, do not reuse changed-path of another version' ' git init doublewrite && test_commit -C doublewrite c "$CENT" && + git -C doublewrite config --add commitgraph.changedPathsVersion 1 && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C doublewrite commit-graph write --reachable --changed-paths && + test_filter_computed 1 trace2.txt && + test_filter_upgraded 0 trace2.txt && + git -C doublewrite commit-graph write --reachable --changed-paths && for v in -2 3 do
@@ -667,8 +677,13 @@ test_expect_success 'when writing commit graph, do not reuse changed-path of ano EOF test_cmp expect err || return 1 done && + git -C doublewrite config --add commitgraph.changedPathsVersion 2 && - git -C doublewrite commit-graph write --reachable --changed-paths && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C doublewrite commit-graph write --reachable --changed-paths && + test_filter_computed 1 trace2.txt && + test_filter_upgraded 0 trace2.txt && + ( cd doublewrite && echo "c01f" >expect &&
@@ -677,6 +692,24 @@ test_expect_success 'when writing commit graph, do not reuse changed-path of ano ) ' +test_expect_success 'when writing commit graph, reuse changed-path of another version where possible' ' + git init upgrade && + + test_commit -C upgrade base no-high-bits && + + git -C upgrade config --add commitgraph.changedPathsVersion 1 && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C upgrade commit-graph write --reachable --changed-paths && + test_filter_computed 1 trace2.txt && + test_filter_upgraded 0 trace2.txt && + + git -C upgrade config --add commitgraph.changedPathsVersion 2 && + GIT_TRACE2_EVENT="$(pwd)/trace2.txt" \ + git -C upgrade commit-graph write --reachable --changed-paths && + test_filter_computed 0 trace2.txt && + test_filter_upgraded 1 trace2.txt +' + corrupt_graph () { test_when_finished "rm -rf $graph" && git commit-graph write --reachable --changed-paths &&
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty
PATCH v5 17/17 bloom: introduce `deinit_bloom_filters()`
In reply to Taylor BlauAfter we are done using Bloom filters, we do not currently clean up any memory allocated by the commit slab used to store those filters in the first place.
Besides the bloom_filter structures themselves, there is mostly nothing to free() in the first place, since in the read-only path all Bloom filter's data members point to a memory mapped region in the commit-graph file itself.
But when generating Bloom filters from scratch (or initializing truncated filters) we allocate additional memory to store the filter's data.
Keep track of when we need to free() this additional chunk of memory by using an extra pointer to_free. Most of the time this will be NULL (indicating that we are representing an existing Bloom filter stored in a memory mapped region). When it is non-NULL, free it before discarding the Bloom filters slab.
--- bloom.c | 16 +++++++++++++++- bloom.h | 3 +++ commit-graph.c | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-)
bloom.c +15 -1
@@ -92,6 +92,7 @@ int load_bloom_filter_from_graph(struct commit_graph *g,sizeof (unsigned char ) * start_index + BLOOMDATA_CHUNK_HEADER_SIZE); filter->version = g->bloom_filter_settings->hash_version; + filter->to_free =NULL ;return 1 ; }
@@ -264,6 +265,18 @@ void init_bloom_filters(void)init_bloom_filter_slab (&bloom_filters); } +static void free_one_bloom_filter (struct bloom_filter *filter) +{ +if (!filter) +return ; +free (filter->to_free); +} + +void deinit_bloom_filters (void ) +{ +deep_clear_bloom_filter_slab (&bloom_filters, free_one_bloom_filter); +} + static int pathmap_cmp(const void *hashmap_cmp_fn_data UNUSED, const struct hashmap_entry *eptr, const struct hashmap_entry *entry_or_key,
@@ -280,7 +293,7 @@ static int pathmap_cmp(const void *hashmap_cmp_fn_data UNUSED,static void init_truncated_large_filter (struct bloom_filter *filter,int version) { - filter->data =xmalloc (1 ); + filter->data = filter->to_free =xmalloc (1 ); filter->data[0 ] =0xFF ; filter->len =1 ; filter->version = version;
@@ -482,6 +495,7 @@ struct bloom_filter *get_or_compute_bloom_filter(struct repository *r, filter->len =1 ; }CALLOC_ARRAY (filter->data, filter->len); + filter->to_free = filter->data;hashmap_for_each_entry (&pathmap, &iter, e, entry) {struct bloom_key key;
bloom.h +3 -0
@@ -56,6 +56,8 @@ struct bloom_filter {unsigned char *data;size_t len;int version; + +void *to_free; };/*
@@ -96,6 +98,7 @@ void add_key_to_filter(const struct bloom_key *key, const struct bloom_filter_settings *settings);void init_bloom_filters (void ); +void deinit_bloom_filters (void );enum bloom_filter_computed {BLOOM_NOT_COMPUTED = (1 <<0 ),
commit-graph.c +4 -0
@@ -830,6 +830,7 @@ struct bloom_filter_settings *get_bloom_filter_settings(struct repository *r)void close_commit_graph (struct raw_object_store *o) {clear_commit_graph_data_slab (&commit_graph_data_slab); +deinit_bloom_filters ();free_commit_graph (o->commit_graph); o->commit_graph =NULL ; }
@@ -2648,6 +2649,9 @@ int write_commit_graph(struct object_directory *odb, res =write_commit_graph_file (ctx); +if (ctx->changed_paths) +deinit_bloom_filters (); +if (ctx->split)mark_commit_graphs (ctx);
Signature · 1 line
2.43.0.334.gd4dbce1db5.dirty