From: Taylor Blau <hidden> Date: 2021-01-19 23:24:52
This series introduces a new mode of 'git repack' where (instead of packing just
loose objects or packing everything together into one pack), the set of packs
left forms a geometric progression by object count.
It does not depend on either series of the revindex patches I sent recently.
Roughly speaking, for a given factor, say "d", each pack has at least "d" times
the number of objects as the next largest pack. So, if there are "N" packs,
"P1", "P2", ..., "PN" ordered by object count (where "PN" has the most objects,
and "P1" the fewest), then:
objects(Pi) > d * objects(P(i-1))
for all 1 < i <= N.
This is done by first ordering packs by object count, and then determining the
longest sequence of large packs which already form a geometric progression. All
packs on the small side of that cut must be repacked together, and so we check
that the existing progression can be maintained with the new pack, and adjust as
necessary.
In actuality, this is approximated in order for 'git repack' to have to create
at most one new pack. The details of this approximation are discussed at length
in the final patch.
'git repack' implements this new option by marking the packs that don't need to
be touched as "frozen" and it does this by marking them as pack_keep_in_core,
and then using a new option pack-objects option '--assume-kept-packs-closed' to
stop the reachability traversal once it encounters any objects in the kept
packs.
When repacking in this mode, the caller implicitly trusts that the unchanged
packs are closed under reachability, and thus they can halt the traversal as
soon as an object in any one of those packs is found.
The first three patches introduce the new revision and pack-objects options
necessary for this to work. The next four patches introduce an MRU cache for
kept packs only. Then a new pack-objects mode is introduced to allow callers to
specify the list of kept packs over stdin in case they are too long to be listed
as arguments. Finally, geometric repacking is introduced
Thanks in advance for your review.
Jeff King (4):
p5303: add missing &&-chains
p5303: measure time to repack with keep
pack-objects: rewrite honor-pack-keep logic
packfile: add kept-pack cache for find_kept_pack_entry()
Taylor Blau (6):
packfile: introduce 'find_kept_pack_entry()'
revision: learn '--no-kept-objects'
builtin/pack-objects.c: learn '--assume-kept-packs-closed'
builtin/pack-objects.c: teach '--keep-pack-stdin'
builtin/repack.c: extract loose object handling
builtin/repack.c: add '--geometric' option
Documentation/git-pack-objects.txt | 19 +++
Documentation/git-repack.txt | 11 ++
Documentation/rev-list-options.txt | 7 +
builtin/pack-objects.c | 161 ++++++++++++++--------
builtin/repack.c | 206 ++++++++++++++++++++++++++---
list-objects.c | 7 +
object-store.h | 10 ++
packfile.c | 69 ++++++++++
packfile.h | 2 +
revision.c | 15 +++
revision.h | 4 +
t/perf/p5303-many-packs.sh | 18 ++-
t/t6114-keep-packs.sh | 128 ++++++++++++++++++
t/t7703-repack-geometric.sh | 81 ++++++++++++
14 files changed, 663 insertions(+), 75 deletions(-)
create mode 100755 t/t6114-keep-packs.sh
create mode 100755 t/t7703-repack-geometric.sh
--
2.30.0.138.g6d7191ea01
From: Taylor Blau <hidden> Date: 2021-01-19 23:25:30
Some callers want to perform a reachability traversal that terminates
when an object is found in a kept pack. The closest existing option is
'--honor-pack-keep', but this isn't quite what we want. Instead of
halting the traversal midway through, a full traversal is always
performed, and the results are only trimmed afterwords.
Besides needing to introduce a new flag (since culling results
post-facto can be different than halting the traversal as it's
happening), there is an additional wrinkle handling the distinction
in-core and on-disk kept packs. That is: what kinds of kept pack should
stop the traversal?
Introduce '--no-kept-objects[=<on-disk|in-core>]' to specify which kinds
of kept packs, if any, should stop a traversal. This can be useful for
callers that want to perform a reachability analysis, but want to leave
certain packs alone (for e.g., when doing a geometric repack that has
some "large" packs it wants to leave alone).
Signed-off-by: Taylor Blau <redacted>
---
Documentation/rev-list-options.txt | 7 +++
list-objects.c | 7 +++
revision.c | 15 +++++++
revision.h | 4 ++
t/t6114-keep-packs.sh | 69 ++++++++++++++++++++++++++++++
5 files changed, 102 insertions(+)
create mode 100755 t/t6114-keep-packs.sh
@@ -856,6 +856,13 @@ ifdef::git-rev-list[] Only useful with `--objects`; print the object IDs that are not in packs.+--no-kept-objects[=<kind>]::+ Halts the traversal as soon as an object in a kept pack is+ found. If `<kind>` is `on-disk`, only packs with a corresponding+ `*.keep` file are ignored. If `<kind>` is `in-core`, only packs+ with their in-core kept state set are ignored. Otherwise, both+ kinds of kept packs are ignored.+ --object-names:: Only useful with `--objects`; print the names of the object IDs that are found. This is the default behavior.
@@ -0,0 +1,69 @@+#!/bin/sh++test_description='rev-list with .keep packs'+../test-lib.sh++test_expect_success'setup''+test_commitloose&&+test_commitpacked&&+test_commitkept&&++KEPT_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/kept+^refs/tags/packed+EOF+)&&+MISC_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/packed+^refs/tags/loose+EOF+)&&++touch.git/objects/pack/pack-$KEPT_PACK.keep+'++rev_list_objects(){+gitrev-list"$@">out&&+sortout+}++idx_objects(){+gitshow-index<$1>expect-idx&&+cut-d" "-f2<expect-idx|sort+}++test_expect_success'--no-kept-objects excludes trees and blobs in .keep packs''+rev_list_objects--objects--all--no-object-names>kept&&+rev_list_objects--objects--all--no-object-names--no-kept-objects>no-kept&&++idx_objects.git/objects/pack/pack-$KEPT_PACK.idx>expect&&+comm-3keptno-kept>actual&&++test_cmpexpectactual+'++test_expect_success'--no-kept-objects excludes kept non-MIDX object''+test_configcore.multiPackIndextrue&&++# Create a pack with just the commit object in pack, and do not mark it+# as kept (even though it appears in $KEPT_PACK, which does have a .keep+# file).+MIDX_PACK=$(gitpack-objects.git/objects/pack/pack<<-EOF+$(gitrev-parsekept)+EOF+)&&++# Write a MIDX containing all packs, but use the version of the commit+# at "kept" in a non-kept pack by touching $MIDX_PACK.+touch.git/objects/pack/pack-$MIDX_PACK.pack&&+gitmulti-pack-indexwrite&&++rev_list_objects--objects--no-object-names--no-kept-objectsHEAD>actual&&+(+idx_objects.git/objects/pack/pack-$MISC_PACK.idx&&+gitrev-list--objects--no-object-namesrefs/tags/loose+)|sort>expect&&+test_cmpexpectactual+'++test_done
From: Taylor Blau <hidden> Date: 2021-01-19 23:26:52
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s). They
could accomplish this by calling 'find_pack_entry()' and checking
whether the found pack is kept or not, but this is insufficient, since
there may be duplicate objects (and the mru cache makes it unpredictable
which variant we'll get).
Teach this new function to treat the two different kinds of kept packs
(on disk ones with .keep files, as well as in-core ones which are set by
manually poking the 'pack_keep_in_core' bit) separately. This will
become important for callers that only want to respect a certain kind of
kept pack.
Introduce 'find_kept_pack_entry()' which behaves like
'find_pack_entry()', except that it skips over packs which are not
marked kept. Callers will be added in subsequent patches.
Co-authored-by: Jeff King [off-list ref]
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
packfile.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-----
packfile.h | 6 +++++
2 files changed, 65 insertions(+), 5 deletions(-)
From: Taylor Blau <hidden> Date: 2021-01-19 23:26:52
From: Jeff King <redacted>
In a recent patch we added a function 'find_kept_pack_entry()' to look
for an object only among kept packs.
While this function avoids doing any lookup work in non-kept packs, it
is still linear in the number of packs, since we have to traverse the
linked list of packs once per object. Let's cache a reduced version of
that list to save us time.
Note that this cache will last the lifetime of the program. We could
invalidate it on reprepare_packed_git(), but there's not much point in
being rigorous here:
- we might already fail to notice new .keep packs showing up after the
program starts. We only reprepare_packed_git() when we fail to find
an object. But adding a new pack won't cause that to happen.
Somebody repacking could add a new pack and delete an old one, but
most of the time we'd have a descriptor or mmap open to the old
pack anyway, so we might not even notice.
- in pack-objects we already cache the .keep state at startup, since
56dfeb6263 (pack-objects: compute local/ignore_pack_keep early,
2016-07-29). So this is just extending that concept further.
- we don't have to worry about any packed_git being removed; we always
keep the old structs around, even after reprepare_packed_git()
Here are p5303 results (as always, measured against the kernel):
Test HEAD^ HEAD
------------------------------------------------------------------------------------
5303.5: repack (1) 56.87(54.63+10.48) 56.63(54.41+10.36) -0.4%
5303.6: repack with keep (1) 1.26(1.19+0.06) 1.25(1.19+0.05) -0.8%
5303.10: repack (50) 89.35(132.42+6.25) 89.49(132.31+6.31) +0.2%
5303.11: repack with keep (50) 6.73(26.61+0.59) 6.72(26.70+0.53) -0.1%
5303.15: repack (1000) 217.25(494.38+15.24) 218.69(495.62+14.99) +0.7%
5303.16: repack with keep (1000) 133.12(311.80+8.44) 128.79(306.96+8.55) -3.3%
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 4 +-
object-store.h | 10 ++++
packfile.c | 103 +++++++++++++++++++++++------------------
packfile.h | 4 --
revision.c | 8 ++--
5 files changed, 75 insertions(+), 54 deletions(-)
@@ -150,6 +158,8 @@ struct raw_object_store {/* A most-recently-used ordered version of the packed_git list. */structlist_headpacked_git_mru;+structkept_pack_cache*kept_pack_cache;+/**Amapofpackfilestopacked_gitstructsfortrackingwhich*packshavebeenloadedalready.
From: Taylor Blau <hidden> Date: 2021-01-19 23:26:52
From: Jeff King <redacted>
Now that we have find_kept_pack_entry(), we don't have to manually keep
hunting through every pack to find a possible "kept" duplicate of the
object. This should be faster, assuming only a portion of your total
packs are actually kept.
Note that we have to re-order the logic a bit here; we can deal with the
"kept" situation completely, and then just fall back to the "--local"
question. It might be worth having a similar optimized function to look
at only local packs.
Here are the results from p5303 (measurements taken on git.git):
Test HEAD^ HEAD
------------------------------------------------------------------------------------
5303.5: repack (1) 57.29(54.88+10.39) 56.87(54.63+10.48) -0.7%
5303.6: repack with keep (1) 1.25(1.19+0.05) 1.26(1.19+0.06) +0.8%
5303.10: repack (50) 89.71(132.78+6.14) 89.35(132.42+6.25) -0.4%
5303.11: repack with keep (50) 6.92(26.93+0.58) 6.73(26.61+0.59) -2.7%
5303.15: repack (1000) 217.14(493.76+15.29) 217.25(494.38+15.24) +0.1%
5303.16: repack with keep (1000) 209.46(387.83+8.42) 133.12(311.80+8.44) -36.4%
So our case with many packs and a .keep is finally now faster than the
non-keep case (because it gets the speed benefit of looking at fewer
objects, but not as big a penalty for looking at many packs).
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 125 ++++++++++++++++++++++++-----------------
1 file changed, 73 insertions(+), 52 deletions(-)
From: Taylor Blau <hidden> Date: 2021-01-19 23:27:52
From: Jeff King <redacted>
This is the same as the regular repack test, except that we mark the
single base pack as "kept" and use --assume-kept-packs-closed. The
theory is that this should be faster than the normal repack, because
we'll have fewer objects to traverse and process.
And indeed, it is much faster in the single-pack case (all timings
measured on the kernel):
5303.5: repack (1) 57.29(54.88+10.39)
5303.6: repack with keep (1) 1.25(1.19+0.05)
and in the 50-pack case:
5303.10: repack (50) 89.71(132.78+6.14)
5303.11: repack with keep (50) 6.92(26.93+0.58)
but our improvements vanish as we approach 1000 packs.
5303.15: repack (1000) 217.14(493.76+15.29)
5303.16: repack with keep (1000) 209.46(387.83+8.42)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Our solution to that was to notice that most repos don't have keep
files, and to make that case a fast path. But as soon as you add a
single .keep, that part of pack-objects slows down again (even if we
have fewer objects total to look at).
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
@@ -27,8 +27,11 @@ repack_into_n () {>pushes&&# create base packfile-head-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack&&+base_pack=$(+head-n1pushes|+gitpack-objects--delta-base-offset--revsstaging/pack+)&&+test_exportbase_pack&&# and then incrementals between each pair of commitslast=&&
@@ -87,6 +90,15 @@ do--reflog--indexed-objects--delta-base-offset\--stdout</dev/null>/dev/null'++test_perf"repack with keep ($nr_packs)"'+gitpack-objects--keep-true-parents\+--honor-pack-keep--assume-kept-packs-closed\+--keep-pack=pack-$base_pack.pack\+--non-empty--all\+--reflog--indexed-objects--delta-base-offset\+--stdout</dev/null>/dev/null+'done# Measure pack loading with 10,000 packs.
From: Taylor Blau <hidden> Date: 2021-01-19 23:28:06
Add a shortcut to specify '--keep-pack=<pack-name>' arguments over
stdin, in case a caller wishes to indicate more kept packs than the
argument limit will allow.
Passing this option overrides any other option to 'git pack-objects'
that takes input over stdin. For example, '--revs' still forces a
reachability traversal, but will not accept any revision arguments over
stdin. Use of '--keep-pack-stdin' within Git is limited to one caller
(added in a subsequent patch) which does not pass any other input over
stdin.
No new tests are added here, since a caller from 'git repack' will
exercise these options in a subsequent patch.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 8 ++++++++
builtin/pack-objects.c | 23 ++++++++++++++++++++---
2 files changed, 28 insertions(+), 3 deletions(-)
@@ -135,6 +135,14 @@ depth is 4095. leading directory (e.g. `pack-123.pack`). The option could be specified multiple times to keep multiple packs.+--keep-pack-stdin::+ Take a list of line-delimited `<pack-name>` arguments, treating+ them as if they were each passed as `--keep-pack=<pack-name>`.+ Useful for when many packs are being kept to avoid argument+ length limitations. Requires that `--revs` be passed or implied,+ but does not allow the caller to pass additional traversal+ arguments over standard input.+ --assume-kept-packs-closed:: This flag causes `git rev-list` to halt the object traversal when it encounters an object found in a kept pack. This is
@@ -3487,6 +3487,15 @@ static int option_parse_unpack_unreachable(const struct option *opt,return0;}+staticvoidcollect_kept_packs(structstring_list*keep_pack_list)+{+structstrbufbuf=STRBUF_INIT;+while(strbuf_getline(&buf,stdin)!=EOF)+string_list_append(keep_pack_list,+strbuf_detach(&buf,NULL));+strbuf_release(&buf);+}+intcmd_pack_objects(intargc,constchar**argv,constchar*prefix){intuse_internal_rev_list=0;
@@ -3496,6 +3505,7 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)intrev_list_unpacked=0,rev_list_all=0,rev_list_reflog=0;intrev_list_index=0;structstring_listkeep_pack_list=STRING_LIST_INIT_NODUP;+intkeep_pack_stdin=0;structoptionpack_objects_options[]={OPT_SET_INT('q',"quiet",&progress,N_("do not show progress meter"),0),
@@ -3568,6 +3578,8 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)N_("assume the union of kept packs is closed under reachability")),OPT_STRING_LIST(0,"keep-pack",&keep_pack_list,N_("name"),N_("ignore this pack")),+OPT_BOOL(0,"keep-pack-stdin",&keep_pack_stdin,+N_("read the list of kept packs from stdin")),OPT_INTEGER(0,"compression",&pack_compression_level,N_("pack compression level")),OPT_SET_INT(0,"keep-true-parents",&grafts_replace_parents,
From: Taylor Blau <hidden> Date: 2021-01-19 23:28:43
Often it is useful to both:
- have relatively few packfiles in a repository, and
- avoid having so few packfiles in a repository that we repack its
entire contents regularly
This patch implements a '--geometric=<n>' option in 'git repack'. This
allows the caller to specify that they would like each pack to be at
least a factor times as large as the previous largest pack (by object
count).
Concretely, say that a repository has 'n' packfiles, labeled P1, P2,
..., up to Pn. Each packfile has an object count equal to 'objects(Pn)'.
With a geometric factor of 'r', it should be that:
objects(Pi) > r*objects(P(i-1))
for all i in [1, n], where the packs are sorted by
objects(P1) <= objects(P2) <= ... <= objects(Pn).
Since finding a true optimal repacking is NP-hard, we approximate it
along two directions:
1. We assume that there is a cutoff of packs _before starting the
repack_ where everything to the right of that cut-off already forms
a geometric progression (or no cutoff exists and everything must be
repacked).
2. We assume that everything smaller than the cutoff count must be
repacked. This forms our base assumption, but it can also cause
even the "heavy" packs to get repacked, for e.g., if we have 6
packs containing the following number of objects:
1, 1, 1, 2, 4, 32
then we would place the cutoff between '1, 1' and '1, 2, 4, 32',
rolling up the first two packs into a pack with 2 objects. That
breaks our progression and leaves us:
2, 1, 2, 4, 32
^
(where the '^' indicates the position of our split). To restore a
progression, we move the split forward (towards larger packs)
joining each pack into our new pack until a geometric progression
is restored. Here, that looks like:
2, 1, 2, 4, 32 ~> 3, 2, 4, 32 ~> 5, 4, 32 ~> ... ~> 9, 32
^ ^ ^ ^
This has the advantage of not repacking the heavy-side of packs too
often while also only creating one new pack at a time. Another wrinkle
is that we assume that loose, indexed, and reflog'd objects are
insignificant, and lump them into any new pack that we create. This can
lead to non-idempotent results.
Suggested-by: Derrick Stolee <redacted>
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-repack.txt | 11 +++
builtin/repack.c | 165 ++++++++++++++++++++++++++++++++++-
t/t7703-repack-geometric.sh | 81 +++++++++++++++++
3 files changed, 256 insertions(+), 1 deletion(-)
create mode 100755 t/t7703-repack-geometric.sh
@@ -165,6 +165,17 @@ depth is 4095. Pass the `--delta-islands` option to `git-pack-objects`, see linkgit:git-pack-objects[1].+-g=<factor>::+--geometric=<factor>::+ Arrange resulting pack structure so that each successive pack+ contains at least `<factor>` times the number of objects as the+ next-largest pack.+++`git repack` ensures this by determining a "cut" of packfiles that need to be+repacked into one in order to ensure a geometric progression. It picks the+smallest set of packfiles such that as many of the larger packfiles (by count of+objects contained in that pack) may be left intact.+ Configuration -------------
@@ -378,6 +490,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)N_("repack objects in packs marked with .keep")),OPT_STRING_LIST(0,"keep-pack",&keep_pack_list,N_("name"),N_("do not repack this pack")),+OPT_INTEGER('g',"geometric",&geometric_factor,+N_("find a geometric progression with factor <N>")),OPT_END()};
@@ -0,0 +1,81 @@+#!/bin/sh++test_description='git repack --geometric works correctly'++../test-lib.sh++GIT_TEST_MULTI_PACK_INDEX=0++objdir=.git/objects+midx=$objdir/pack/multi-pack-index++test_expect_success'--geometric with an intact progression''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# These packs already form a geometric progression.+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=22&&# 6 objects+test_commit_bulk--start=44&&# 12 objects++find$objdir/pack-name"*.pack"|sort>expect&&+GIT_TEST_MULTI_PACK_BITMAP=0gitrepack--geometric2-d&&+find$objdir/pack-name"*.pack"|sort>actual&&++test_cmpexpectactual+)+'++test_expect_success'--geometric with small-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+find$objdir/pack-name"*.pack"|sort>small&&+test_commit_bulk--start=34&&# 12 objects+test_commit_bulk--start=78&&# 24 objects+find$objdir/pack-name"*.pack"|sort>before&&++GIT_TEST_MULTI_PACK_BITMAP=0gitrepack--geometric2-d&&++# Three packs in total; two of the existing large ones, and one+# new one.+find$objdir/pack-name"*.pack"|sort>after&&+test_line_count=3after&&+comm-3smallbefore|tr-d"\t">large&&+grep-qFflargeafter+)+'++test_expect_success'--geometric with small- and large-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# size(small1) + size(small2) > size(medium) / 2+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+test_commit_bulk--start=23&&# 7 objects+test_commit_bulk--start=69&&# 27 objects &&++find$objdir/pack-name"*.pack"|sort>before&&++GIT_TEST_MULTI_PACK_BITMAP=0gitrepack--geometric2-d&&++find$objdir/pack-name"*.pack"|sort>after&&+comm-12beforeafter>untouched&&++# Two packs in total; the largest pack from before running "git+# repack", and one new one.+test_line_count=1untouched&&+test_line_count=2after+)+'++test_done
From: Taylor Blau <hidden> Date: 2021-01-19 23:29:04
Teach pack-objects an option to imply the revision machinery's new
'--no-kept-objects' option when doing a reachability traversal.
When '--assume-kept-packs-closed' is given as an argument to
pack-objects, it behaves differently (i.e., passes different options to
the ensuing revision walk) depending on whether or not other arguments
are passed:
- If the caller also specifies a '--keep-pack' argument (to mark a
pack as kept in-core), then assume that this combination means to
stop traversal only at in-core packs.
- If instead the caller passes '--honor-pack-keep', then assume that
the caller wants to stop traversal only at packs with a
corresponding .keep file (consistent with the original meaning which
only refers to packs with a .keep file).
- If both '--keep-pack' and '--honor-pack-keep' are passed, then
assume the caller wants to stop traversal at either kind of kept
pack.
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 11 ++++++
builtin/pack-objects.c | 13 +++++++
t/t6114-keep-packs.sh | 59 ++++++++++++++++++++++++++++++
3 files changed, 83 insertions(+)
@@ -135,6 +135,17 @@ depth is 4095. leading directory (e.g. `pack-123.pack`). The option could be specified multiple times to keep multiple packs.+--assume-kept-packs-closed::+ This flag causes `git rev-list` to halt the object traversal+ when it encounters an object found in a kept pack. This is+ dissimilar to `--honor-pack-keep`, which only prunes unwanted+ results after the full traversal is completed.+++Without any `--keep-pack=<pack-name>` arguments, only packs with an+on-disk `*.keep` files are used when considering when to halt the+traversal. If other packs are artificially marked as "kept" with+`--keep-pack`, then those are considered as well.+ --incremental:: This flag causes an object already in a pack to be ignored even if it would have otherwise been packed.
@@ -78,6 +78,7 @@ static int have_non_local_packs;staticintincremental;staticintignore_packed_keep_on_disk;staticintignore_packed_keep_in_core;+staticintassume_kept_packs_closed;staticintallow_ofs_delta;staticstructpack_idx_optionpack_idx_opts;staticconstchar*base_name;
@@ -3542,6 +3543,8 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)N_("create packs suitable for shallow fetches")),OPT_BOOL(0,"honor-pack-keep",&ignore_packed_keep_on_disk,N_("ignore packs that have companion .keep file")),+OPT_BOOL(0,"assume-kept-packs-closed",&assume_kept_packs_closed,+N_("assume the union of kept packs is closed under reachability")),OPT_STRING_LIST(0,"keep-pack",&keep_pack_list,N_("name"),N_("ignore this pack")),OPT_INTEGER(0,"compression",&pack_compression_level,
From: Taylor Blau <hidden> Date: 2021-01-19 23:29:39
'git repack -g' will have to learn about unreachable loose objects that
need to be removed in a separate path from the existing checks.
Extract that check into a function so it can be called from multiple
places.
Signed-off-by: Taylor Blau <redacted>
---
builtin/repack.c | 41 +++++++++++++++++++++++++----------------
1 file changed, 25 insertions(+), 16 deletions(-)
From: Taylor Blau <hidden> Date: 2021-01-19 23:29:44
From: Jeff King <redacted>
These are in a helper function, so the usual chain-lint doesn't notice
them. This function is still not perfect, as it has some git invocations
on the left-hand-side of the pipe, but it's primary purpose is timing,
not finding bugs or correctness issues.
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -24,11 +24,11 @@ repack_into_n () {sed-n'1~5p'|head-n"$1"|perl-e'print reverse <>'\->pushes+>pushes&&# create base packfilehead-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack+gitpack-objects--delta-base-offset--revsstaging/pack&&# and then incrementals between each pair of commitslast=&&
From: Taylor Blau <hidden> Date: 2021-01-20 14:35:54
On Wed, Jan 20, 2021 at 08:59:48AM -0500, Derrick Stolee wrote:
On 1/19/2021 6:24 PM, Taylor Blau wrote:
quoted
'git repack -g' will have to learn about unreachable loose objects that
This reference to the '-g' option is one patch too early. Perhaps
say
An upcoming patch will introduce geometric repacking. This will
require removing unreachable loose objects in a separate path
from the existing checks.
or similar?
Mmm. I had imagined that this would be read either in the context of
this series, or by someone in the future long after 'git repack -g' had
been introduced.
I could see that it's confusing, though, and I do agree your wording
makes clearer that the option doesn't exist yet.
I'm happy to send a replacement or reroll if you feel strongly, but in
either case I'll wait for a little more review first.
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2021-01-20 14:51:55
On Wed, Jan 20, 2021 at 08:40:22AM -0500, Derrick Stolee wrote:
On 1/19/2021 6:24 PM, Taylor Blau wrote:
quoted
for (m = r->objects->multi_pack_index; m; m = m->next) {
- if (fill_midx_entry(r, oid, e, m))
+ if (!(fill_midx_entry(r, oid, e, m)))
nit: we don't need extra parens around fill_midx_entry().
Yep. I checked whether we should have written this as "if
(fill_midx_entry(...) < 0)", but fill_midx_entry returns a positive
number on error, so checking "!fill_midx_entry" is certainly what we
should be doing.
quoted
- if (!p->multi_pack_index && fill_pack_entry(oid, e, p)) {
- list_move(&p->mru, &r->objects->packed_git_mru);
- return 1;
+ if (p->multi_pack_index && !kept_only) {
+ /*
+ * If this pack is covered by the MIDX, we'd have found
+ * the object already in the loop above if it was here,
+ * so don't bother looking.
+ *
+ * The exception is if we are looking only at kept
+ * packs. An object can be present in two packs covered
+ * by the MIDX, one kept and one not-kept. And as the
+ * MIDX points to only one copy of each object, it might
+ * have returned only the non-kept version above. We
+ * have to check again to be thorough.
+ */
+ continue;
+ }
+ if (!kept_only ||
+ (((kept_only & ON_DISK_KEEP_PACKS) && p->pack_keep) ||
+ ((kept_only & IN_CORE_KEEP_PACKS) && p->pack_keep_in_core))) {
+ if (fill_pack_entry(oid, e, p)) {
+ list_move(&p->mru, &r->objects->packed_git_mru);
+ return 1;
+ }
Here is the meat of your patch. The comment helps a lot.
This might have been easier if the MIDX had preferred kept packs
over non-kept packs (before sorting by modified time). Perhaps
the MIDX could get an extra field to say "I preferred kept packs"
which would let us trust the MIDX return here without the pack
loop.
(Note: we can't just change the MIDX selection and then start
trusting all MIDXs to have the right tie-breakers because of
existing files in the wild.)
Yeah, that is what makes it tricky. Changing the code isn't so hard: a
new field that we check and do one of two things when we're breaking
ties.
But I think the cognitive load is high, and I'm not sure that the
benefit (skipping another linear pass through non-MIDX'd packs when
looking up an object in kept packs only _and_ that object is duplicated)
is worth the extra hassle with the MIDX code.
All of that said, I do think that it's worth revisiting this and giving
it some more thought after multi-pack bitmaps to see whether we feel the
same or not.
Thanks,
Taylor
On Wed, Jan 20, 2021 at 08:59:48AM -0500, Derrick Stolee wrote:
quoted
On 1/19/2021 6:24 PM, Taylor Blau wrote:
quoted
'git repack -g' will have to learn about unreachable loose objects that
This reference to the '-g' option is one patch too early. Perhaps
say
An upcoming patch will introduce geometric repacking. This will
require removing unreachable loose objects in a separate path
from the existing checks.
or similar?
Mmm. I had imagined that this would be read either in the context of
this series, or by someone in the future long after 'git repack -g' had
been introduced.
I could see that it's confusing, though, and I do agree your wording
makes clearer that the option doesn't exist yet.
I'm happy to send a replacement or reroll if you feel strongly, but in
either case I'll wait for a little more review first.
Definitely don't rush a re-roll for my nit-picks.
Thanks,
-Stolee
This series introduces a new mode of 'git repack' where (instead of packing just
loose objects or packing everything together into one pack), the set of packs
left forms a geometric progression by object count.
...
Thanks in advance for your review.
I had the pleasure of reading an early version of this series, but it's
been a while. Upon a fresh reading, I only had nitpicks. Otherwise, this
LGTM.
I encourage other reviewers to read patch 10 carefully, as that is the
most math-heavy of all of them.
Thanks,
-Stolee
'git repack -g' will have to learn about unreachable loose objects that
This reference to the '-g' option is one patch too early. Perhaps
say
An upcoming patch will introduce geometric repacking. This will
require removing unreachable loose objects in a separate path
from the existing checks.
or similar?
Thanks,
-Stolee
for (m = r->objects->multi_pack_index; m; m = m->next) {
- if (fill_midx_entry(r, oid, e, m))
+ if (!(fill_midx_entry(r, oid, e, m)))
nit: we don't need extra parens around fill_midx_entry().
- if (!p->multi_pack_index && fill_pack_entry(oid, e, p)) {
- list_move(&p->mru, &r->objects->packed_git_mru);
- return 1;
+ if (p->multi_pack_index && !kept_only) {
+ /*
+ * If this pack is covered by the MIDX, we'd have found
+ * the object already in the loop above if it was here,
+ * so don't bother looking.
+ *
+ * The exception is if we are looking only at kept
+ * packs. An object can be present in two packs covered
+ * by the MIDX, one kept and one not-kept. And as the
+ * MIDX points to only one copy of each object, it might
+ * have returned only the non-kept version above. We
+ * have to check again to be thorough.
+ */
+ continue;
+ }
+ if (!kept_only ||
+ (((kept_only & ON_DISK_KEEP_PACKS) && p->pack_keep) ||
+ ((kept_only & IN_CORE_KEEP_PACKS) && p->pack_keep_in_core))) {
+ if (fill_pack_entry(oid, e, p)) {
+ list_move(&p->mru, &r->objects->packed_git_mru);
+ return 1;
+ }
Here is the meat of your patch. The comment helps a lot.
This might have been easier if the MIDX had preferred kept packs
over non-kept packs (before sorting by modified time). Perhaps
the MIDX could get an extra field to say "I preferred kept packs"
which would let us trust the MIDX return here without the pack
loop.
(Note: we can't just change the MIDX selection and then start
trusting all MIDXs to have the right tie-breakers because of
existing files in the wild.)
Thanks,
-Stolee
From: Junio C Hamano <hidden> Date: 2021-01-29 02:34:00
Taylor Blau [off-list ref] writes:
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s). They
could accomplish this by calling 'find_pack_entry()' and checking
whether the found pack is kept or not, but this is insufficient, since
there may be duplicate objects (and the mru cache makes it unpredictable
which variant we'll get).
I wonder if we eventually need a callback interface to walk _all_
pack entries for a given object, so that "I am only interested in
instances in kept packs" will be under total control of the callers.
As it stands, it is "just grab any one that is in a kept pack, any
one of them is fine", which is almost just of as narrow utility as
the original's "just grab the first one---any one of them is fine",
the latter of which is "insufficient" as the log message says.
But this (in the context of the remainder of the series) might be
sufficient, at least for now.
Teach this new function to treat the two different kinds of kept packs
(on disk ones with .keep files, as well as in-core ones which are set by
manually poking the 'pack_keep_in_core' bit) separately. This will
become important for callers that only want to respect a certain kind of
kept pack.
Or maybe not ;-)
If there are notable relationship between on-disk and in-core kept
packs (e.g. "the set of on-disk kept packs is a subset of in-core
kept packs", "usually on-disk kept packs get in-core kept bit upon
their packed_git instances are populated, but we can drop the bit at
runtime, so on-disk and in-core are pretty much independent and
there is no notable relationship"), it must be explained upfront to
help the reader form a sensible world view.
Introduce 'find_kept_pack_entry()' which behaves like
'find_pack_entry()', except that it skips over packs which are not
marked kept. Callers will be added in subsequent patches.
Co-authored-by: Jeff King [off-list ref]
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
From: Taylor Blau <hidden> Date: 2021-01-29 18:39:41
On Thu, Jan 28, 2021 at 06:33:10PM -0800, Junio C Hamano wrote:
Taylor Blau [off-list ref] writes:
quoted
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s). They
could accomplish this by calling 'find_pack_entry()' and checking
whether the found pack is kept or not, but this is insufficient, since
there may be duplicate objects (and the mru cache makes it unpredictable
which variant we'll get).
I wonder if we eventually need a callback interface to walk _all_
pack entries for a given object, so that "I am only interested in
instances in kept packs" will be under total control of the callers.
As it stands, it is "just grab any one that is in a kept pack, any
one of them is fine", which is almost just of as narrow utility as
the original's "just grab the first one---any one of them is fine",
the latter of which is "insufficient" as the log message says.
But this (in the context of the remainder of the series) might be
sufficient, at least for now.
As you note, it's more about "can I find this object in any kept pack
(of a certain kind)" versus, "show me this object in a pack" (and hope
that if it appears in a kept pack, that that's the copy that is picked).
quoted
Teach this new function to treat the two different kinds of kept packs
(on disk ones with .keep files, as well as in-core ones which are set by
manually poking the 'pack_keep_in_core' bit) separately. This will
become important for callers that only want to respect a certain kind of
kept pack.
Or maybe not ;-)
:-). The difference here is that we will only want to stop the traversal
at packs which are considered to be stable from the perspective of a
geometric repack.
We mark those packs as "stable" by setting their in-core kept bit, but
we don't write ".keep" files (which would make them on-disk kept). The
latter is up to the user, not us.
If there are notable relationship between on-disk and in-core kept
packs (e.g. "the set of on-disk kept packs is a subset of in-core
kept packs", "usually on-disk kept packs get in-core kept bit upon
their packed_git instances are populated, but we can drop the bit at
runtime, so on-disk and in-core are pretty much independent and
there is no notable relationship"), it must be explained upfront to
help the reader form a sensible world view.
Unfortunately, I don't think that there is a sensible world-view here
to be formed. Honestly, the distinction between .keep packs and in-core
kept packs is incredibly narrow, and I find our separate handling of
them awkward and error-prone.
But, it is sort of what you'd want here (i.e., a way to mark all objects
in a pack as ignored without actually writing the physical file that
says "ignore all objects in this pack").
Thanks,
Taylor
From: Jeff King <hidden> Date: 2021-01-29 19:33:10
On Thu, Jan 28, 2021 at 06:33:10PM -0800, Junio C Hamano wrote:
Taylor Blau [off-list ref] writes:
quoted
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s). They
could accomplish this by calling 'find_pack_entry()' and checking
whether the found pack is kept or not, but this is insufficient, since
there may be duplicate objects (and the mru cache makes it unpredictable
which variant we'll get).
I wonder if we eventually need a callback interface to walk _all_
pack entries for a given object, so that "I am only interested in
instances in kept packs" will be under total control of the callers.
As it stands, it is "just grab any one that is in a kept pack, any
one of them is fine", which is almost just of as narrow utility as
the original's "just grab the first one---any one of them is fine",
the latter of which is "insufficient" as the log message says.
We do that already in pack-objects, and that's the problem: it's really
slow. So if you have few kept packs, but a lot of other ones, you'd like
to pre-split the packs into two lists, and not bother walking the one
you know won't turn up interesting results.
I think the commit message here doesn't emphasize that reasoning enough.
It talks about using "find_pack_entry()", and that is definitely not
sufficient for our purposes. But the interesting part is replacing the
existing "walk all packs and see if any were kept" logic, which happens
in patch 6.
So the more compelling argument, I think, is something like:
- you sometimes want to know if object X is any kept packs
- you can't use find_pack_entry(), because it only gives you the first
pack it finds
- you can walk over all packs and look for the object in each.
pack-objects does this. But it's slow, because you are looking in
packs you don't care about.
- so it's helpful for the lookup to know up front which packs are
interesting to find objects in and which are not, to avoid looking
in the uninteresting ones
-Peff
From: Taylor Blau <hidden> Date: 2021-02-04 04:00:16
Here is an updated version of mine and Peff's series to add a new 'git repack
--geometric' mode which supports repacking a repository into a geometric
progression of packs by object count.
This version depends on jk/p5303-sed-portability-fix, but it could be applied
onto 'master' after resolving a trivial conflict.
As a reminder, here is a description from the original cover letter [1] which
outlines what the geometric mode entails:
Roughly speaking, for a given factor, say "d", each pack has at least "d"
times the number of objects as the next largest pack. So, if there are "N"
packs, "P1", "P2", ..., "PN" ordered by object count (where "PN" has the most
objects, and "P1" the fewest), then:
objects(Pi) > d * objects(P(i-1))
for all 1 < i <= N.
This is done by first ordering packs by object count, and then determining the
longest sequence of large packs which already form a geometric progression.
All packs on the small side of that cut must be repacked together, and so we
check that the existing progression can be maintained with the new pack, and
adjust as necessary.
Since last time, the series has been reworked substantially. In the previous
version, a single reachability traversal was performed to determine the set of
objects to pack. That traversal halted upon encountering any objects found in a
kept pack, but this led to serious correctness problems (if, for e.g., an object
we would like to pack is an ancestor of some other object in a kept pack, and
thus isn't picked up).
The details of the new approach can be found in the third patch, but the gist is
as follows:
- 'git repack --geometric' calls 'git pack-objects --stdin-packs', which
expects input like:
pack-xyz.pack
pack-abc.pack
^pack-exclude.pack
'git pack-objects' determines the set of objects to pack by iterating all of
the objects in the listed packs, and then removing any objects found in the
packs which are prefixed with '^'.
- To improve the delta selection process, the same reachability traversal from
the original version of this series is performed. But, the set of objects to
pack is already known, so we don't run the risk of the correctness bugs from
before.
- In this reachability traversal, visited objects get their namehash field
set, which helps drive the heuristics that power delta selection. It's
possible that we may not visit all of the objects to pack, but that's OK
since this process is only additive (again, the set of objects to pack is
known up-front independent of the reachability traversal).
So, this strikes a happy medium between not relying on reachability so much that
we run the risk of corrupting the repository, but relying on it enough that we
can aid in the delta selection process.
Because we reuse the same "halt the traversal when encountering objects in kept
packs" mechanism, a lot of the patches are able to be reused. The structure of
the series is as follows:
- The first three patches introduce new infrastructure, and implement 'git
pack-objects --stdin-packs'.
- The next four patches introduce and use a kept-pack cache, which improves
the performance of 'git pack-objects --stdin-packs' substantially.
- The final patch implements 'git repack --geometric'.
Let me know what you think of this new approach, and thanks in advance for your
review.
[1]: https://lore.kernel.org/git/cover.1611098616.git.me@ttaylorr.com/
Thanks in advance for your review.
Jeff King (4):
p5303: add missing &&-chains
p5303: measure time to repack with keep
builtin/pack-objects.c: rewrite honor-pack-keep logic
packfile: add kept-pack cache for find_kept_pack_entry()
Taylor Blau (4):
packfile: introduce 'find_kept_pack_entry()'
revision: learn '--no-kept-objects'
builtin/pack-objects.c: add '--stdin-packs' option
builtin/repack.c: add '--geometric' option
Documentation/git-pack-objects.txt | 10 +
Documentation/git-repack.txt | 11 ++
Documentation/rev-list-options.txt | 7 +
builtin/pack-objects.c | 301 +++++++++++++++++++++++------
builtin/repack.c | 187 +++++++++++++++++-
list-objects.c | 7 +
object-store.h | 10 +
packfile.c | 69 +++++++
packfile.h | 2 +
revision.c | 15 ++
revision.h | 4 +
t/perf/p5303-many-packs.sh | 24 ++-
t/t5300-pack-object.sh | 97 ++++++++++
t/t6114-keep-packs.sh | 69 +++++++
t/t7703-repack-geometric.sh | 137 +++++++++++++
15 files changed, 889 insertions(+), 61 deletions(-)
create mode 100755 t/t6114-keep-packs.sh
create mode 100755 t/t7703-repack-geometric.sh
Range-diff against v1:
1: dc7fa4c7a6 ! 1: f7186147eb packfile: introduce 'find_kept_pack_entry()'
@@ Commit message
packfile: introduce 'find_kept_pack_entry()'
Future callers will want a function to fill a 'struct pack_entry' for a
- given object id but _only_ from its position in any kept pack(s). They
- could accomplish this by calling 'find_pack_entry()' and checking
- whether the found pack is kept or not, but this is insufficient, since
- there may be duplicate objects (and the mru cache makes it unpredictable
- which variant we'll get).
-
- Teach this new function to treat the two different kinds of kept packs
- (on disk ones with .keep files, as well as in-core ones which are set by
- manually poking the 'pack_keep_in_core' bit) separately. This will
- become important for callers that only want to respect a certain kind of
- kept pack.
-
- Introduce 'find_kept_pack_entry()' which behaves like
- 'find_pack_entry()', except that it skips over packs which are not
- marked kept. Callers will be added in subsequent patches.
+ given object id but _only_ from its position in any kept pack(s).
+
+ In particular, an new 'git repack' mode which ensures the resulting
+ packs form a geometric progress by object count will mark packs that it
+ does not want to repack as "kept in-core", and it will want to halt a
+ reachability traversal as soon as it visits an object in any of the kept
+ packs. But, it does not want to halt the traversal at non-kept, or
+ .keep packs.
+
+ The obvious alternative is 'find_pack_entry()', but this doesn't quite
+ suffice since it only returns the first pack it finds, which may or may
+ not be kept (and the mru cache makes it unpredictable which one you'll
+ get if there are options).
+
+ Short of that, you could walk over all packs looking for the object in
+ each one, but it scales with the number of packs, which may be
+ prohibitive.
+
+ Introduce 'find_kept_pack_entry()', a function which is like
+ 'find_pack_entry()', but only fills in objects in the kept packs.
+
+ Handle packs which have .keep files, as well as in-core kept packs
+ separately, since certain callers will want to distinguish one from the
+ other. (Though on-disk and in-core kept packs share the adjective
+ "kept", it is best to think of the two sets as independent.)
+
+ There is a gotcha when looking up objects that are duplicated in kept
+ and non-kept packs, particularly when the MIDX stores the non-kept
+ version and the caller asked for kept objects only. This could be
+ resolved by teaching the MIDX to resolve duplicates by always favoring
+ the kept pack (if one exists), but this breaks an assumption in existing
+ MIDXs, and so it would require a format change.
+
+ The benefit to changing the MIDX in this way is marginal, so we instead
+ have a more thorough check here which is explained with a comment.
+
+ Callers will be added in subsequent patches.
Co-authored-by: Jeff King [off-list ref]
Signed-off-by: Jeff King [off-list ref]
@@ packfile.c: int find_pack_entry(struct repository *r, const struct object_id *oi
for (m = r->objects->multi_pack_index; m; m = m->next) {
- if (fill_midx_entry(r, oid, e, m))
-+ if (!(fill_midx_entry(r, oid, e, m)))
++ if (!fill_midx_entry(r, oid, e, m))
+ continue;
+
+ if (!kept_only)
2: 4184529648 ! 2: ddc2896caa revision: learn '--no-kept-objects'
@@ Metadata
## Commit message ##
revision: learn '--no-kept-objects'
- Some callers want to perform a reachability traversal that terminates
- when an object is found in a kept pack. The closest existing option is
- '--honor-pack-keep', but this isn't quite what we want. Instead of
- halting the traversal midway through, a full traversal is always
- performed, and the results are only trimmed afterwords.
+ A future caller will want to be able to perform a reachability traversal
+ which terminates when visiting an object found in a kept pack. The
+ closest existing option is '--honor-pack-keep', but this isn't quite
+ what we want. Instead of halting the traversal midway through, a full
+ traversal is always performed, and the results are only trimmed
+ afterwords.
Besides needing to introduce a new flag (since culling results
post-facto can be different than halting the traversal as it's
@@ Commit message
of kept packs, if any, should stop a traversal. This can be useful for
callers that want to perform a reachability analysis, but want to leave
certain packs alone (for e.g., when doing a geometric repack that has
- some "large" packs it wants to leave alone).
+ some "large" packs which are kept in-core that it wants to leave alone).
Signed-off-by: Taylor Blau [off-list ref]
3: 2da42e9ca2 < -: ---------- builtin/pack-objects.c: learn '--assume-kept-packs-closed'
-: ---------- > 3: c96b1bf995 builtin/pack-objects.c: add '--stdin-packs' option
4: 26b46dff15 ! 4: a46b7002b4 p5303: add missing &&-chains
@@ Commit message
## t/perf/p5303-many-packs.sh ##
@@ t/perf/p5303-many-packs.sh: repack_into_n () {
- sed -n '1~5p' |
- head -n "$1" |
- perl -e 'print reverse <>' \
-- >pushes
-+ >pushes &&
+ push @commits, $_ if $. % 5 == 1;
+ }
+ print reverse @commits;
+- ' "$1" >pushes
++ ' "$1" >pushes &&
# create base packfile
head -n 1 pushes |
5: b3b2574d4d < -: ---------- p5303: measure time to repack with keep
-: ---------- > 5: b5081c01b5 p5303: measure time to repack with keep
6: 4dd5076fcc ! 6: c3868c7df9 pack-objects: rewrite honor-pack-keep logic
@@ Metadata
Author: Jeff King [off-list ref]
## Commit message ##
- pack-objects: rewrite honor-pack-keep logic
+ builtin/pack-objects.c: rewrite honor-pack-keep logic
Now that we have find_kept_pack_entry(), we don't have to manually keep
hunting through every pack to find a possible "kept" duplicate of the
@@ Commit message
question. It might be worth having a similar optimized function to look
at only local packs.
- Here are the results from p5303 (measurements taken on git.git):
+ Here are the results from p5303 (measurements again taken on the
+ kernel):
- Test HEAD^ HEAD
- ------------------------------------------------------------------------------------
- 5303.5: repack (1) 57.29(54.88+10.39) 56.87(54.63+10.48) -0.7%
- 5303.6: repack with keep (1) 1.25(1.19+0.05) 1.26(1.19+0.06) +0.8%
- 5303.10: repack (50) 89.71(132.78+6.14) 89.35(132.42+6.25) -0.4%
- 5303.11: repack with keep (50) 6.92(26.93+0.58) 6.73(26.61+0.59) -2.7%
- 5303.15: repack (1000) 217.14(493.76+15.29) 217.25(494.38+15.24) +0.1%
- 5303.16: repack with keep (1000) 209.46(387.83+8.42) 133.12(311.80+8.44) -36.4%
+ Test HEAD^ HEAD
+ -----------------------------------------------------------------------------------------------
+ 5303.5: repack (1) 57.42(54.88+10.64) 57.44(54.71+10.78) +0.0%
+ 5303.6: repack with --stdin-packs (1) 0.01(0.01+0.00) 0.01(0.00+0.01) +0.0%
+ 5303.10: repack (50) 71.26(88.24+4.96) 71.32(88.38+4.90) +0.1%
+ 5303.11: repack with --stdin-packs (50) 3.49(11.82+0.28) 3.43(11.81+0.22) -1.7%
+ 5303.15: repack (1000) 215.64(491.33+14.80) 215.59(493.75+14.62) -0.0%
+ 5303.16: repack with --stdin-packs (1000) 198.79(380.51+7.97) 131.44(314.24+8.11) -33.9%
- So our case with many packs and a .keep is finally now faster than the
+ So our --stdin-packs case with many packs is now finally faster than the
non-keep case (because it gets the speed benefit of looking at fewer
objects, but not as big a penalty for looking at many packs).
7: 182664e1a9 ! 7: f1c07324f6 packfile: add kept-pack cache for find_kept_pack_entry()
@@ Commit message
Here are p5303 results (as always, measured against the kernel):
- Test HEAD^ HEAD
- ------------------------------------------------------------------------------------
- 5303.5: repack (1) 56.87(54.63+10.48) 56.63(54.41+10.36) -0.4%
- 5303.6: repack with keep (1) 1.26(1.19+0.06) 1.25(1.19+0.05) -0.8%
- 5303.10: repack (50) 89.35(132.42+6.25) 89.49(132.31+6.31) +0.2%
- 5303.11: repack with keep (50) 6.73(26.61+0.59) 6.72(26.70+0.53) -0.1%
- 5303.15: repack (1000) 217.25(494.38+15.24) 218.69(495.62+14.99) +0.7%
- 5303.16: repack with keep (1000) 133.12(311.80+8.44) 128.79(306.96+8.55) -3.3%
+ Test HEAD^ HEAD
+ ----------------------------------------------------------------------------------------------
+ 5303.5: repack (1) 57.44(54.71+10.78) 57.06(54.29+10.96) -0.7%
+ 5303.6: repack with --stdin-packs (1) 0.01(0.00+0.01) 0.01(0.01+0.00) +0.0%
+ 5303.10: repack (50) 71.32(88.38+4.90) 71.47(88.60+5.04) +0.2%
+ 5303.11: repack with --stdin-packs (50) 3.43(11.81+0.22) 3.49(12.21+0.26) +1.7%
+ 5303.15: repack (1000) 215.59(493.75+14.62) 217.41(495.36+14.85) +0.8%
+ 5303.16: repack with --stdin-packs (1000) 131.44(314.24+8.11) 126.75(309.88+8.09) -3.6%
Signed-off-by: Jeff King [off-list ref]
Signed-off-by: Taylor Blau [off-list ref]
@@ builtin/pack-objects.c: static int want_found_object(const struct object_id *oid
if (ignore_packed_keep_on_disk && p->pack_keep)
return 0;
+@@ builtin/pack-objects.c: static void read_packs_list_from_stdin(void)
+ * an optimization during delta selection.
+ */
+ revs.no_kept_objects = 1;
+- revs.keep_pack_cache_flags |= IN_CORE_KEEP_PACKS;
++ revs.keep_pack_cache_flags |= CACHE_IN_CORE_KEEP_PACKS;
+ revs.blob_objects = 1;
+ revs.tree_objects = 1;
+ revs.tag_objects = 1;
## object-store.h ##
@@ object-store.h: static inline int pack_map_entry_cmp(const void *unused_cmp_data,
@@ packfile.c: static int find_one_pack_entry(struct repository *r,
return 0;
for (m = r->objects->multi_pack_index; m; m = m->next) {
-- if (!(fill_midx_entry(r, oid, e, m)))
+- if (!fill_midx_entry(r, oid, e, m))
- continue;
-
- if (!kept_only)
8: 6547c082f8 < -: ---------- builtin/pack-objects.c: teach '--keep-pack-stdin'
9: a808fbdf31 < -: ---------- builtin/repack.c: extract loose object handling
10: f853087216 ! 8: d5561585c2 builtin/repack.c: add '--geometric' option
@@ builtin/repack.c: static void repack_promisor_objects(const struct pack_objects_
+ geometry = *geometry_p;
+
+ for (p = get_all_packs(the_repository); p; p = p->next) {
++ if (!pack_kept_objects && p->pack_keep)
++ continue;
++
+ ALLOC_GROW(geometry->pack,
+ geometry->pack_nr + 1,
+ geometry->pack_alloc);
@@ builtin/repack.c: static void repack_promisor_objects(const struct pack_objects_
+ uint32_t split;
+ off_t total_size = 0;
+
++ if (geometry->pack_nr <= 1) {
++ geometry->split = geometry->pack_nr;
++ return;
++ }
++
+ split = geometry->pack_nr - 1;
+
+ /*
@@ builtin/repack.c: static void repack_promisor_objects(const struct pack_objects_
+ geometry->split = 0;
+}
+
- static void handle_loose_and_reachable(struct child_process *cmd,
- const char *unpack_unreachable,
- int pack_everything,
+ int cmd_repack(int argc, const char **argv, const char *prefix)
+ {
+ struct child_process cmd = CHILD_PROCESS_INIT;
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
struct string_list names = STRING_LIST_INIT_DUP;
struct string_list rollback = STRING_LIST_INIT_NODUP;
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix
die(_(incremental_bitmap_conflict_error));
+ if (geometric_factor) {
++ if (pack_everything)
++ die(_("--geometric is incompatible with -A, -a"));
+ init_pack_geometry(&geometry);
+ split_pack_geometry(geometry, geometric_factor);
+ }
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix
packtmp = mkpathdup("%s/.tmp-%d-pack", packdir, (int)getpid());
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
- handle_loose_and_reachable(&cmd, unpack_unreachable,
- pack_everything,
- keep_unreachable);
+ strvec_pushf(&cmd.args, "--keep-pack=%s",
+ keep_pack_list.items[i].string);
+ strvec_push(&cmd.args, "--non-empty");
+- strvec_push(&cmd.args, "--all");
+- strvec_push(&cmd.args, "--reflog");
+- strvec_push(&cmd.args, "--indexed-objects");
++ if (!geometry) {
++ /*
++ * 'git pack-objects' will up all objects loose or packed
++ * (either rolling them up or leaving them alone), so don't pass
++ * these options.
++ *
++ * The implementation of 'git pack-objects --stdin-packs'
++ * makes them redundant (and the two are incompatible).
++ */
++ strvec_push(&cmd.args, "--all");
++ strvec_push(&cmd.args, "--reflog");
++ strvec_push(&cmd.args, "--indexed-objects");
++ }
+ if (has_promisor_remote())
+ strvec_push(&cmd.args, "--exclude-promisor-objects");
+ if (write_bitmaps > 0)
+@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix)
+ strvec_push(&cmd.env_array, "GIT_REF_PARANOIA=1");
+ }
+ }
+ } else if (geometry) {
-+ strvec_push(&cmd.args, "--keep-pack-stdin");
-+ strvec_push(&cmd.args, "--honor-pack-keep");
-+ strvec_push(&cmd.args, "--assume-kept-packs-closed");
-+ if (delete_redundant)
-+ handle_loose_and_reachable(&cmd, unpack_unreachable,
-+ pack_everything,
-+ keep_unreachable);
++ strvec_push(&cmd.args, "--stdin-packs");
++ strvec_push(&cmd.args, "--unpacked");
} else {
strvec_push(&cmd.args, "--unpacked");
strvec_push(&cmd.args, "--incremental");
@@ builtin/repack.c: int cmd_repack(int argc, const char **argv, const char *prefix
+ if (geometry) {
+ FILE *in = xfdopen(cmd.in, "w");
+ /*
-+ * Tell 'git pack-objects' to avoid tampering with the structure
-+ * with the packs that already form a geometric progression.
-+ *
-+ * Everything else will get picked up by the reachability walk.
++ * The resulting pack should contain all objects in packs that
++ * are going to be rolled up, but exclude objects in packs which
++ * are being left alone.
+ */
-+ for (i = geometry->split; i < geometry->pack_nr; i++)
++ for (i = 0; i < geometry->split; i++)
+ fprintf(in, "%s\n", pack_basename(geometry->pack[i]));
++ for (i = geometry->split; i < geometry->pack_nr; i++)
++ fprintf(in, "^%s\n", pack_basename(geometry->pack[i]));
+ fclose(in);
+ }
+
@@ t/t7703-repack-geometric.sh (new)
+objdir=.git/objects
+midx=$objdir/pack/multi-pack-index
+
++test_expect_success '--geometric with no packs' '
++ git init geometric &&
++ test_when_finished "rm -fr geometric" &&
++ (
++ cd geometric &&
++
++ git repack --geometric 2 >out &&
++ test_i18ngrep "Nothing new to pack" out
++ )
++'
++
+test_expect_success '--geometric with an intact progression' '
+ git init geometric &&
+ test_when_finished "rm -fr geometric" &&
@@ t/t7703-repack-geometric.sh (new)
+ test_commit_bulk --start=4 4 && # 12 objects
+
+ find $objdir/pack -name "*.pack" | sort >expect &&
-+ GIT_TEST_MULTI_PACK_BITMAP=0 git repack --geometric 2 -d &&
++ git repack --geometric 2 -d &&
+ find $objdir/pack -name "*.pack" | sort >actual &&
+
+ test_cmp expect actual
@@ t/t7703-repack-geometric.sh (new)
+ test_commit_bulk --start=7 8 && # 24 objects
+ find $objdir/pack -name "*.pack" | sort >before &&
+
-+ GIT_TEST_MULTI_PACK_BITMAP=0 git repack --geometric 2 -d &&
++ git repack --geometric 2 -d &&
+
+ # Three packs in total; two of the existing large ones, and one
+ # new one.
@@ t/t7703-repack-geometric.sh (new)
+
+ find $objdir/pack -name "*.pack" | sort >before &&
+
-+ GIT_TEST_MULTI_PACK_BITMAP=0 git repack --geometric 2 -d &&
++ git repack --geometric 2 -d &&
+
+ find $objdir/pack -name "*.pack" | sort >after &&
+ comm -12 before after >untouched &&
@@ t/t7703-repack-geometric.sh (new)
+ )
+'
+
++test_expect_success '--geometric ignores kept packs' '
++ git init geometric &&
++ test_when_finished "rm -fr geometric" &&
++ (
++ cd geometric &&
++
++ test_commit kept && # 3 objects
++ test_commit pack && # 3 objects
++
++ KEPT=$(git pack-objects --revs $objdir/pack/pack <<-EOF
++ refs/tags/kept
++ EOF
++ ) &&
++ PACK=$(git pack-objects --revs $objdir/pack/pack <<-EOF
++ refs/tags/pack
++ ^refs/tags/kept
++ EOF
++ ) &&
++
++ # neither pack contains more than twice the number of objects in
++ # the other, so they should be combined. but, marking one as
++ # .kept on disk will "freeze" it, so the pack structure should
++ # remain unchanged.
++ touch $objdir/pack/pack-$KEPT.keep &&
++
++ find $objdir/pack -name "*.pack" | sort >before &&
++ git repack --geometric 2 -d &&
++ find $objdir/pack -name "*.pack" | sort >after &&
++
++ # both packs should still exist
++ test_path_is_file $objdir/pack/pack-$KEPT.pack &&
++ test_path_is_file $objdir/pack/pack-$PACK.pack &&
++
++ # and no new packs should be created
++ test_cmp before after &&
++
++ # Passing --pack-kept-objects causes packs with a .keep file to
++ # be repacked, too.
++ git repack --geometric 2 -d --pack-kept-objects &&
++
++ find $objdir/pack -name "*.pack" >after &&
++ test_line_count = 1 after
++ )
++'
++
+test_done
--
2.30.0.533.g2f8b6b552f.dirty
From: Taylor Blau <hidden> Date: 2021-02-04 04:00:21
From: Jeff King <redacted>
These are in a helper function, so the usual chain-lint doesn't notice
them. This function is still not perfect, as it has some git invocations
on the left-hand-side of the pipe, but it's primary purpose is timing,
not finding bugs or correctness issues.
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -28,11 +28,11 @@ repack_into_n () {push@commits,$_if$.%5==1;}printreverse@commits;-'"$1">pushes+'"$1">pushes&&# create base packfilehead-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack+gitpack-objects--delta-base-offset--revsstaging/pack&&# and then incrementals between each pair of commitslast=&&
From: Taylor Blau <hidden> Date: 2021-02-04 04:00:27
In an upcoming commit, 'git repack' will want to create a pack comprised
of all of the objects in some packs (the included packs) excluding any
objects in some other packs (the excluded packs).
This caller could iterate those packs themselves and feed the objects it
finds to 'git pack-objects' directly over stdin, but this approach has a
few downsides:
- It requires every caller that wants to drive 'git pack-objects' in
this way to implement pack iteration themselves. This forces the
caller to think about details like what order objects are fed to
pack-objects, which callers would likely rather not do.
- If the set of objects in included packs is large, it requires
sending a lot of data over a pipe, which is inefficient.
- The caller is forced to keep track of the excluded objects, too, and
make sure that it doesn't send any objects that appear in both
included and excluded packs.
But the biggest downside is the lack of a reachability traversal.
Because the caller passes in a list of objects directly, those objects
don't get a namehash assigned to them, which can have a negative impact
on the delta selection process, causing 'git pack-objects' to fail to
find good deltas even when they exist.
The caller could formulate a reachability traversal themselves, but the
only way to drive 'git pack-objects' in this way is to do a full
traversal, and then remove objects in the excluded packs after the
traversal is complete. This can be detrimental to callers who care
about performance, especially in repositories with many objects.
Introduce 'git pack-objects --stdin-packs' which remedies these four
concerns.
'git pack-objects --stdin-packs' expects a list of pack names on stdin,
where 'pack-xyz.pack' denotes that pack as included, and
'^pack-xyz.pack' denotes it as excluded. The resulting pack includes all
objects that are present in at least one included pack, and aren't
present in any excluded pack.
To address the delta selection problem, 'git pack-objects --stdin-packs'
works as follows. First, it assembles a list of objects that it is going
to pack, as above. Then, a reachability traversal is started, whose tips
are any commits mentioned in included packs. Upon visiting an object, we
find its corresponding object_entry in the to_pack list, and set its
namehash parameter appropriately.
To avoid the traversal visiting more objects than it needs to, the
traversal is halted upon encountering an object which can be found in an
excluded pack (by marking the excluded packs as kept in-core, and
passing --no-kept-objects=in-core to the revision machinery).
This can cause the traversal to halt early, for example if an object in
an included pack is an ancestor of ones in excluded packs. But stopping
early is OK, since filling in the namehash fields of objects in the
to_pack list is only additive (i.e., having it helps the delta selection
process, but leaving it blank doesn't impact the correctness of the
resulting pack).
Even still, it is unlikely that this hurts us much in practice, since
the 'git repack --geometric' caller (which is introduced in a later
commit) marks small packs as included, and large ones as excluded.
During ordinary use, the small packs usually represent pushes after a
large repack, and so are unlikely to be ancestors of objects that
already exist in the repository.
(I found it convenient while developing this patch to have 'git
pack-objects' report the number of objects which were visited and got
their namehash fields filled in during traversal. This is also included
in the below patch via trace2 data lines).
Suggested-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 10 ++
builtin/pack-objects.c | 176 ++++++++++++++++++++++++++++-
t/t5300-pack-object.sh | 97 ++++++++++++++++
3 files changed, 281 insertions(+), 2 deletions(-)
@@ -85,6 +85,16 @@ base-name:: reference was included in the resulting packfile. This can be useful to send new tags to native Git clients.+--stdin-packs::+ Read the basenames of packfiles from the standard input, instead+ of object names or revision arguments. The resulting pack+ contains all objects listed in the included packs (those not+ beginning with `^`), excluding any objects listed in the+ excluded packs (beginning with `^`).+++Incompatible with `--revs`, or options that imply `--revs` (such as+`--all`), with the exception of `--unpacked`, which is compatible.+ --window=<n>:: --depth=<n>:: These two options affect how the objects contained in
@@ -2979,6 +2979,164 @@ static int git_pack_config(const char *k, const char *v, void *cb)returngit_default_config(k,v,cb);}+staticintstdin_packs_found_nr;+staticintstdin_packs_hints_nr;++staticintadd_object_entry_from_pack(conststructobject_id*oid,+structpacked_git*p,+uint32_tpos,+void*_data)+{+structrev_info*revs=_data;+structobject_infooi=OBJECT_INFO_INIT;+off_tofs;+enumobject_typetype;++display_progress(progress_state,++nr_seen);++ofs=nth_packed_object_offset(p,pos);++oi.typep=&type;+if(packed_object_info(the_repository,p,ofs,&oi)<0)+die(_("could not get type of object %s in pack %s"),+oid_to_hex(oid),p->pack_name);+elseif(type==OBJ_COMMIT){+/*+*commitsinincludedpacksareusedasstartingpointsforthe+*subsequentrevisionwalk+*/+add_pending_oid(revs,NULL,oid,0);+}++if(have_duplicate_entry(oid,0))+return0;++if(!want_object_in_pack(oid,0,&p,&ofs))+return0;++stdin_packs_found_nr++;++create_object_entry(oid,type,0,0,0,p,ofs);++return0;+}++staticvoidshow_commit_pack_hint(structcommit*commit,void*_data)+{+}++staticvoidshow_object_pack_hint(structobject*object,constchar*name,+void*_data)+{+structobject_entry*oe=packlist_find(&to_pack,&object->oid);+if(!oe)+return;++/*+*Our'to_pack'listwasconstructedbyiteratingallobjectspackedin+*includedpacks,andsodoesn'thaveanon-zerohashfieldthatyou+*wouldtypicallypickupduringareachabilitytraversal.+*+*Makeabest-effortattempttofillinthe->hashand->no_try_delta+*hereusinganowinordertoperhapsimprovethedeltaselection+*process.+*/+oe->hash=pack_name_hash(name);+oe->no_try_delta=name&&no_try_delta(name);++stdin_packs_hints_nr++;+}++staticvoidread_packs_list_from_stdin(void)+{+structstrbufbuf=STRBUF_INIT;+structstring_listinclude_packs=STRING_LIST_INIT_DUP;+structstring_listexclude_packs=STRING_LIST_INIT_DUP;+structstring_list_item*item=NULL;++structpacked_git*p;+structrev_inforevs;++repo_init_revisions(the_repository,&revs,NULL);+/*+*Usearevisionwalktofillinthenamehashofobjectsintheinclude+*packs.Tosavetime,we'llavoidtraversingthroughobjectsthatare+*inexcludedpacks.+*+*Thatmaycauseustoavoidpopulatingallofthenamehashfieldsof+*allincludedobjects,butourgoalisbest-effort,sincethisisonly+*anoptimizationduringdeltaselection.+*/+revs.no_kept_objects=1;+revs.keep_pack_cache_flags|=IN_CORE_KEEP_PACKS;+revs.blob_objects=1;+revs.tree_objects=1;+revs.tag_objects=1;++while(strbuf_getline(&buf,stdin)!=EOF){+if(!buf.len)+continue;++if(*buf.buf=='^')+string_list_append(&exclude_packs,buf.buf+1);+else+string_list_append(&include_packs,buf.buf);++strbuf_reset(&buf);+}++string_list_sort(&include_packs);+string_list_sort(&exclude_packs);++for(p=get_all_packs(the_repository);p;p=p->next){+constchar*pack_name=pack_basename(p);++item=string_list_lookup(&include_packs,pack_name);+if(!item)+item=string_list_lookup(&exclude_packs,pack_name);++if(item)+item->util=p;+}++/*+*Firsthandlealloftheexcludedpacks,markingthemaskeptin-core+*sothatlatercallstoadd_object_entry()discardsanyobjectsthat+*arealsofoundinexcludedpacks.+*/+for_each_string_list_item(item,&exclude_packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+p->pack_keep_in_core=1;+}+for_each_string_list_item(item,&include_packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+for_each_object_in_pack(p,+add_object_entry_from_pack,+&revs,+FOR_EACH_OBJECT_PACK_ORDER);+}++if(prepare_revision_walk(&revs))+die(_("revision walk setup failed"));+traverse_commit_list(&revs,+show_commit_pack_hint,+show_object_pack_hint,+NULL);++trace2_data_intmax("pack-objects",the_repository,"stdin_packs_found",+stdin_packs_found_nr);+trace2_data_intmax("pack-objects",the_repository,"stdin_packs_hints",+stdin_packs_hints_nr);++strbuf_release(&buf);+string_list_clear(&include_packs,0);+string_list_clear(&exclude_packs,0);+}+staticvoidread_object_list_from_stdin(void){charline[GIT_MAX_HEXSZ+1+PATH_MAX+2];
@@ -3532,6 +3691,8 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)OPT_SET_INT_F(0,"indexed-objects",&rev_list_index,N_("include objects referred to by the index"),1,PARSE_OPT_NONEG),+OPT_BOOL(0,"stdin-packs",&stdin_packs,+N_("read packs from stdin")),OPT_BOOL(0,"stdout",&pack_to_stdout,N_("output pack to stdout")),OPT_BOOL(0,"include-tag",&include_tag,
@@ -3681,8 +3842,13 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(filter_options.choice){if(!pack_to_stdout)die(_("cannot use --filter without --stdout"));+if(stdin_packs)+die(_("cannot use --filter with --stdin-packs"));}+if(stdin_packs&&use_internal_rev_list)+die(_("cannot use internal rev list with --stdin-packs"));+/**"soft"reasonsnottousebitmaps-foron-diskrepackbydefaultwewant*
@@ -532,4 +532,101 @@ test_expect_success 'prefetch objects' 'test_line_count=1donelines'+test_expect_success'setup for --stdin-packs tests''+gitinitstdin-packs&&+(+cdstdin-packs&&++test_commitA&&+test_commitB&&+test_commitC&&++foridinABC+do+gitpack-objects.git/objects/pack/pack-$id\+--incremental--revs<<-EOF+refs/tags/$id+EOF+done&&++ls-la.git/objects/pack+)+'++test_expect_success'--stdin-packs with excluded packs''+(+cdstdin-packs&&++PACK_A="$(basename.git/objects/pack/pack-A-*.pack)"&&+PACK_B="$(basename.git/objects/pack/pack-B-*.pack)"&&+PACK_C="$(basename.git/objects/pack/pack-C-*.pack)"&&++gitpack-objectstest--stdin-packs<<-EOF&&+$PACK_A+^$PACK_B+$PACK_C+EOF++(+gitshow-index<$(ls.git/objects/pack/pack-A-*.idx)&&+gitshow-index<$(ls.git/objects/pack/pack-C-*.idx)+)>expect.raw&&+gitshow-index<$(lstest-*.idx)>actual.raw&&++cut-d" "-f2<expect.raw|sort>expect&&+cut-d" "-f2<actual.raw|sort>actual&&+test_cmpexpectactual+)+'++test_expect_success'--stdin-packs is incompatible with --filter''+(+cdstdin-packs&&+test_must_failgitpack-objects--stdin-packs--stdout\+--filter=blob:none</dev/null2>err&&+test_i18ngrep"cannot use --filter with --stdin-packs"err+)+'++test_expect_success'--stdin-packs is incompatible with --revs''+(+cdstdin-packs&&+test_must_failgitpack-objects--stdin-packs--revsout\+</dev/null2>err&&+test_i18ngrep"cannot use internal rev list with --stdin-packs"err+)+'++test_expect_success'--stdin-packs with loose objects''+(+cdstdin-packs&&++PACK_A="$(basename.git/objects/pack/pack-A-*.pack)"&&+PACK_B="$(basename.git/objects/pack/pack-B-*.pack)"&&+PACK_C="$(basename.git/objects/pack/pack-C-*.pack)"&&++test_commitD&&# loose++gitpack-objectstest2--stdin-packs--unpacked<<-EOF&&+$PACK_A+^$PACK_B+$PACK_C+EOF++(+gitshow-index<$(ls.git/objects/pack/pack-A-*.idx)&&+gitshow-index<$(ls.git/objects/pack/pack-C-*.idx)&&+gitrev-list--objects--no-object-names\+refs/tags/C..refs/tags/D++)>expect.raw&&+ls-la.&&+gitshow-index<$(lstest2-*.idx)>actual.raw&&++cut-d" "-f2<expect.raw|sort>expect&&+cut-d" "-f2<actual.raw|sort>actual&&+test_cmpexpectactual+)+'+ test_done
From: Taylor Blau <hidden> Date: 2021-02-04 04:00:29
A future caller will want to be able to perform a reachability traversal
which terminates when visiting an object found in a kept pack. The
closest existing option is '--honor-pack-keep', but this isn't quite
what we want. Instead of halting the traversal midway through, a full
traversal is always performed, and the results are only trimmed
afterwords.
Besides needing to introduce a new flag (since culling results
post-facto can be different than halting the traversal as it's
happening), there is an additional wrinkle handling the distinction
in-core and on-disk kept packs. That is: what kinds of kept pack should
stop the traversal?
Introduce '--no-kept-objects[=<on-disk|in-core>]' to specify which kinds
of kept packs, if any, should stop a traversal. This can be useful for
callers that want to perform a reachability analysis, but want to leave
certain packs alone (for e.g., when doing a geometric repack that has
some "large" packs which are kept in-core that it wants to leave alone).
Signed-off-by: Taylor Blau <redacted>
---
Documentation/rev-list-options.txt | 7 +++
list-objects.c | 7 +++
revision.c | 15 +++++++
revision.h | 4 ++
t/t6114-keep-packs.sh | 69 ++++++++++++++++++++++++++++++
5 files changed, 102 insertions(+)
create mode 100755 t/t6114-keep-packs.sh
@@ -861,6 +861,13 @@ ifdef::git-rev-list[] Only useful with `--objects`; print the object IDs that are not in packs.+--no-kept-objects[=<kind>]::+ Halts the traversal as soon as an object in a kept pack is+ found. If `<kind>` is `on-disk`, only packs with a corresponding+ `*.keep` file are ignored. If `<kind>` is `in-core`, only packs+ with their in-core kept state set are ignored. Otherwise, both+ kinds of kept packs are ignored.+ --object-names:: Only useful with `--objects`; print the names of the object IDs that are found. This is the default behavior.
@@ -0,0 +1,69 @@+#!/bin/sh++test_description='rev-list with .keep packs'+../test-lib.sh++test_expect_success'setup''+test_commitloose&&+test_commitpacked&&+test_commitkept&&++KEPT_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/kept+^refs/tags/packed+EOF+)&&+MISC_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/packed+^refs/tags/loose+EOF+)&&++touch.git/objects/pack/pack-$KEPT_PACK.keep+'++rev_list_objects(){+gitrev-list"$@">out&&+sortout+}++idx_objects(){+gitshow-index<$1>expect-idx&&+cut-d" "-f2<expect-idx|sort+}++test_expect_success'--no-kept-objects excludes trees and blobs in .keep packs''+rev_list_objects--objects--all--no-object-names>kept&&+rev_list_objects--objects--all--no-object-names--no-kept-objects>no-kept&&++idx_objects.git/objects/pack/pack-$KEPT_PACK.idx>expect&&+comm-3keptno-kept>actual&&++test_cmpexpectactual+'++test_expect_success'--no-kept-objects excludes kept non-MIDX object''+test_configcore.multiPackIndextrue&&++# Create a pack with just the commit object in pack, and do not mark it+# as kept (even though it appears in $KEPT_PACK, which does have a .keep+# file).+MIDX_PACK=$(gitpack-objects.git/objects/pack/pack<<-EOF+$(gitrev-parsekept)+EOF+)&&++# Write a MIDX containing all packs, but use the version of the commit+# at "kept" in a non-kept pack by touching $MIDX_PACK.+touch.git/objects/pack/pack-$MIDX_PACK.pack&&+gitmulti-pack-indexwrite&&++rev_list_objects--objects--no-object-names--no-kept-objectsHEAD>actual&&+(+idx_objects.git/objects/pack/pack-$MISC_PACK.idx&&+gitrev-list--objects--no-object-namesrefs/tags/loose+)|sort>expect&&+test_cmpexpectactual+'++test_done
From: Taylor Blau <hidden> Date: 2021-02-04 04:00:47
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s).
In particular, an new 'git repack' mode which ensures the resulting
packs form a geometric progress by object count will mark packs that it
does not want to repack as "kept in-core", and it will want to halt a
reachability traversal as soon as it visits an object in any of the kept
packs. But, it does not want to halt the traversal at non-kept, or
.keep packs.
The obvious alternative is 'find_pack_entry()', but this doesn't quite
suffice since it only returns the first pack it finds, which may or may
not be kept (and the mru cache makes it unpredictable which one you'll
get if there are options).
Short of that, you could walk over all packs looking for the object in
each one, but it scales with the number of packs, which may be
prohibitive.
Introduce 'find_kept_pack_entry()', a function which is like
'find_pack_entry()', but only fills in objects in the kept packs.
Handle packs which have .keep files, as well as in-core kept packs
separately, since certain callers will want to distinguish one from the
other. (Though on-disk and in-core kept packs share the adjective
"kept", it is best to think of the two sets as independent.)
There is a gotcha when looking up objects that are duplicated in kept
and non-kept packs, particularly when the MIDX stores the non-kept
version and the caller asked for kept objects only. This could be
resolved by teaching the MIDX to resolve duplicates by always favoring
the kept pack (if one exists), but this breaks an assumption in existing
MIDXs, and so it would require a format change.
The benefit to changing the MIDX in this way is marginal, so we instead
have a more thorough check here which is explained with a comment.
Callers will be added in subsequent patches.
Co-authored-by: Jeff King [off-list ref]
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
packfile.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-----
packfile.h | 6 +++++
2 files changed, 65 insertions(+), 5 deletions(-)
From: Taylor Blau <hidden> Date: 2021-02-04 04:01:03
From: Jeff King <redacted>
This is the same as the regular repack test, except that we mark the
single base pack as "kept" and use --assume-kept-packs-closed. The
theory is that this should be faster than the normal repack, because
we'll have fewer objects to traverse and process.
Here are some timings on a recent clone of the kernel. In the
single-pack case, there is nothing do since there are no non-excluded
packs:
5303.5: repack (1) 57.42(54.88+10.64)
5303.6: repack with --stdin-packs (1) 0.01(0.01+0.00)
and in the 50-pack case, it is much faster to use `--stdin-packs`, since
we avoid having to consider any objects in the excluded pack:
5303.10: repack (50) 71.26(88.24+4.96)
5303.11: repack with --stdin-packs (50) 3.49(11.82+0.28)
but our improvements vanish as we approach 1000 packs.
5303.15: repack (1000) 215.64(491.33+14.80)
5303.16: repack with --stdin-packs (1000) 198.79(380.51+7.97)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Our solution to that was to notice that most repos don't have keep
files, and to make that case a fast path. But as soon as you add a
single .keep, that part of pack-objects slows down again (even if we
have fewer objects total to look at).
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
@@ -31,8 +31,11 @@ repack_into_n () {'"$1">pushes&&# create base packfile-head-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack&&+base_pack=$(+head-n1pushes|+gitpack-objects--delta-base-offset--revsstaging/pack+)&&+test_exportbase_pack&&# and then incrementals between each pair of commitslast=&&
@@ -49,6 +52,12 @@ repack_into_n () {last=$revdone<pushes&&+(+findstaging-typef-name'pack-*.pack'|+xargs-n1basename|grep-v"$base_pack"&&+printf"^pack-%s.pack\n"$base_pack+)>stdin.packs+# and install the whole thingrm-f.git/objects/pack/*&&mvstaging/*.git/objects/pack/
@@ -91,6 +100,15 @@ do--reflog--indexed-objects--delta-base-offset\--stdout</dev/null>/dev/null'++test_perf"repack with --stdin-packs ($nr_packs)"'+gitpack-objects\+--keep-true-parents\+--stdin-packs\+--non-empty\+--delta-base-offset\+--stdout<stdin.packs>/dev/null+'done# Measure pack loading with 10,000 packs.
From: Taylor Blau <hidden> Date: 2021-02-04 04:01:23
From: Jeff King <redacted>
In a recent patch we added a function 'find_kept_pack_entry()' to look
for an object only among kept packs.
While this function avoids doing any lookup work in non-kept packs, it
is still linear in the number of packs, since we have to traverse the
linked list of packs once per object. Let's cache a reduced version of
that list to save us time.
Note that this cache will last the lifetime of the program. We could
invalidate it on reprepare_packed_git(), but there's not much point in
being rigorous here:
- we might already fail to notice new .keep packs showing up after the
program starts. We only reprepare_packed_git() when we fail to find
an object. But adding a new pack won't cause that to happen.
Somebody repacking could add a new pack and delete an old one, but
most of the time we'd have a descriptor or mmap open to the old
pack anyway, so we might not even notice.
- in pack-objects we already cache the .keep state at startup, since
56dfeb6263 (pack-objects: compute local/ignore_pack_keep early,
2016-07-29). So this is just extending that concept further.
- we don't have to worry about any packed_git being removed; we always
keep the old structs around, even after reprepare_packed_git()
Here are p5303 results (as always, measured against the kernel):
Test HEAD^ HEAD
----------------------------------------------------------------------------------------------
5303.5: repack (1) 57.44(54.71+10.78) 57.06(54.29+10.96) -0.7%
5303.6: repack with --stdin-packs (1) 0.01(0.00+0.01) 0.01(0.01+0.00) +0.0%
5303.10: repack (50) 71.32(88.38+4.90) 71.47(88.60+5.04) +0.2%
5303.11: repack with --stdin-packs (50) 3.43(11.81+0.22) 3.49(12.21+0.26) +1.7%
5303.15: repack (1000) 215.59(493.75+14.62) 217.41(495.36+14.85) +0.8%
5303.16: repack with --stdin-packs (1000) 131.44(314.24+8.11) 126.75(309.88+8.09) -3.6%
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 6 +--
object-store.h | 10 ++++
packfile.c | 103 +++++++++++++++++++++++------------------
packfile.h | 4 --
revision.c | 8 ++--
5 files changed, 76 insertions(+), 55 deletions(-)
@@ -150,6 +158,8 @@ struct raw_object_store {/* A most-recently-used ordered version of the packed_git list. */structlist_headpacked_git_mru;+structkept_pack_cache*kept_pack_cache;+/**Amapofpackfilestopacked_gitstructsfortrackingwhich*packshavebeenloadedalready.
From: Taylor Blau <hidden> Date: 2021-02-04 04:01:57
Often it is useful to both:
- have relatively few packfiles in a repository, and
- avoid having so few packfiles in a repository that we repack its
entire contents regularly
This patch implements a '--geometric=<n>' option in 'git repack'. This
allows the caller to specify that they would like each pack to be at
least a factor times as large as the previous largest pack (by object
count).
Concretely, say that a repository has 'n' packfiles, labeled P1, P2,
..., up to Pn. Each packfile has an object count equal to 'objects(Pn)'.
With a geometric factor of 'r', it should be that:
objects(Pi) > r*objects(P(i-1))
for all i in [1, n], where the packs are sorted by
objects(P1) <= objects(P2) <= ... <= objects(Pn).
Since finding a true optimal repacking is NP-hard, we approximate it
along two directions:
1. We assume that there is a cutoff of packs _before starting the
repack_ where everything to the right of that cut-off already forms
a geometric progression (or no cutoff exists and everything must be
repacked).
2. We assume that everything smaller than the cutoff count must be
repacked. This forms our base assumption, but it can also cause
even the "heavy" packs to get repacked, for e.g., if we have 6
packs containing the following number of objects:
1, 1, 1, 2, 4, 32
then we would place the cutoff between '1, 1' and '1, 2, 4, 32',
rolling up the first two packs into a pack with 2 objects. That
breaks our progression and leaves us:
2, 1, 2, 4, 32
^
(where the '^' indicates the position of our split). To restore a
progression, we move the split forward (towards larger packs)
joining each pack into our new pack until a geometric progression
is restored. Here, that looks like:
2, 1, 2, 4, 32 ~> 3, 2, 4, 32 ~> 5, 4, 32 ~> ... ~> 9, 32
^ ^ ^ ^
This has the advantage of not repacking the heavy-side of packs too
often while also only creating one new pack at a time. Another wrinkle
is that we assume that loose, indexed, and reflog'd objects are
insignificant, and lump them into any new pack that we create. This can
lead to non-idempotent results.
Suggested-by: Derrick Stolee <redacted>
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-repack.txt | 11 +++
builtin/repack.c | 187 ++++++++++++++++++++++++++++++++++-
t/t7703-repack-geometric.sh | 137 +++++++++++++++++++++++++
3 files changed, 331 insertions(+), 4 deletions(-)
create mode 100755 t/t7703-repack-geometric.sh
@@ -165,6 +165,17 @@ depth is 4095. Pass the `--delta-islands` option to `git-pack-objects`, see linkgit:git-pack-objects[1].+-g=<factor>::+--geometric=<factor>::+ Arrange resulting pack structure so that each successive pack+ contains at least `<factor>` times the number of objects as the+ next-largest pack.+++`git repack` ensures this by determining a "cut" of packfiles that need to be+repacked into one in order to ensure a geometric progression. It picks the+smallest set of packfiles such that as many of the larger packfiles (by count of+objects contained in that pack) may be left intact.+ Configuration -------------
@@ -355,6 +475,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)N_("repack objects in packs marked with .keep")),OPT_STRING_LIST(0,"keep-pack",&keep_pack_list,N_("name"),N_("do not repack this pack")),+OPT_INTEGER('g',"geometric",&geometric_factor,+N_("find a geometric progression with factor <N>")),OPT_END()};
@@ -381,6 +503,13 @@ int cmd_repack(int argc, const char **argv, const char *prefix)if(write_bitmaps&&!(pack_everything&ALL_INTO_ONE))die(_(incremental_bitmap_conflict_error));+if(geometric_factor){+if(pack_everything)+die(_("--geometric is incompatible with -A, -a"));+init_pack_geometry(&geometry);+split_pack_geometry(geometry,geometric_factor);+}+packdir=mkpathdup("%s/pack",get_object_directory());packtmp=mkpathdup("%s/.tmp-%d-pack",packdir,(int)getpid());
@@ -0,0 +1,137 @@+#!/bin/sh++test_description='git repack --geometric works correctly'++../test-lib.sh++GIT_TEST_MULTI_PACK_INDEX=0++objdir=.git/objects+midx=$objdir/pack/multi-pack-index++test_expect_success'--geometric with no packs''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++gitrepack--geometric2>out&&+test_i18ngrep"Nothing new to pack"out+)+'++test_expect_success'--geometric with an intact progression''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# These packs already form a geometric progression.+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=22&&# 6 objects+test_commit_bulk--start=44&&# 12 objects++find$objdir/pack-name"*.pack"|sort>expect&&+gitrepack--geometric2-d&&+find$objdir/pack-name"*.pack"|sort>actual&&++test_cmpexpectactual+)+'++test_expect_success'--geometric with small-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+find$objdir/pack-name"*.pack"|sort>small&&+test_commit_bulk--start=34&&# 12 objects+test_commit_bulk--start=78&&# 24 objects+find$objdir/pack-name"*.pack"|sort>before&&++gitrepack--geometric2-d&&++# Three packs in total; two of the existing large ones, and one+# new one.+find$objdir/pack-name"*.pack"|sort>after&&+test_line_count=3after&&+comm-3smallbefore|tr-d"\t">large&&+grep-qFflargeafter+)+'++test_expect_success'--geometric with small- and large-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# size(small1) + size(small2) > size(medium) / 2+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+test_commit_bulk--start=23&&# 7 objects+test_commit_bulk--start=69&&# 27 objects &&++find$objdir/pack-name"*.pack"|sort>before&&++gitrepack--geometric2-d&&++find$objdir/pack-name"*.pack"|sort>after&&+comm-12beforeafter>untouched&&++# Two packs in total; the largest pack from before running "git+# repack", and one new one.+test_line_count=1untouched&&+test_line_count=2after+)+'++test_expect_success'--geometric ignores kept packs''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++test_commitkept&&# 3 objects+test_commitpack&&# 3 objects++KEPT=$(gitpack-objects--revs$objdir/pack/pack<<-EOF+refs/tags/kept+EOF+)&&+PACK=$(gitpack-objects--revs$objdir/pack/pack<<-EOF+refs/tags/pack+^refs/tags/kept+EOF+)&&++# neither pack contains more than twice the number of objects in+# the other, so they should be combined. but, marking one as+# .kept on disk will "freeze" it, so the pack structure should+# remain unchanged.+touch$objdir/pack/pack-$KEPT.keep&&++find$objdir/pack-name"*.pack"|sort>before&&+gitrepack--geometric2-d&&+find$objdir/pack-name"*.pack"|sort>after&&++# both packs should still exist+test_path_is_file$objdir/pack/pack-$KEPT.pack&&+test_path_is_file$objdir/pack/pack-$PACK.pack&&++# and no new packs should be created+test_cmpbeforeafter&&++# Passing --pack-kept-objects causes packs with a .keep file to+# be repacked, too.+gitrepack--geometric2-d--pack-kept-objects&&++find$objdir/pack-name"*.pack">after&&+test_line_count=1after+)+'++test_done
From: Taylor Blau <hidden> Date: 2021-02-04 04:02:11
From: Jeff King <redacted>
Now that we have find_kept_pack_entry(), we don't have to manually keep
hunting through every pack to find a possible "kept" duplicate of the
object. This should be faster, assuming only a portion of your total
packs are actually kept.
Note that we have to re-order the logic a bit here; we can deal with the
"kept" situation completely, and then just fall back to the "--local"
question. It might be worth having a similar optimized function to look
at only local packs.
Here are the results from p5303 (measurements again taken on the
kernel):
Test HEAD^ HEAD
-----------------------------------------------------------------------------------------------
5303.5: repack (1) 57.42(54.88+10.64) 57.44(54.71+10.78) +0.0%
5303.6: repack with --stdin-packs (1) 0.01(0.01+0.00) 0.01(0.00+0.01) +0.0%
5303.10: repack (50) 71.26(88.24+4.96) 71.32(88.38+4.90) +0.1%
5303.11: repack with --stdin-packs (50) 3.49(11.82+0.28) 3.43(11.81+0.22) -1.7%
5303.15: repack (1000) 215.64(491.33+14.80) 215.59(493.75+14.62) -0.0%
5303.16: repack with --stdin-packs (1000) 198.79(380.51+7.97) 131.44(314.24+8.11) -33.9%
So our --stdin-packs case with many packs is now finally faster than the
non-keep case (because it gets the speed benefit of looking at fewer
objects, but not as big a penalty for looking at many packs).
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 125 ++++++++++++++++++++++++-----------------
1 file changed, 73 insertions(+), 52 deletions(-)
From: Jeff King <hidden> Date: 2021-02-16 21:43:21
On Wed, Feb 03, 2021 at 10:58:50PM -0500, Taylor Blau wrote:
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s).
In particular, an new 'git repack' mode which ensures the resulting
Nit (not worth re-rolling): s/an new/a new/
There is a gotcha when looking up objects that are duplicated in kept
and non-kept packs, particularly when the MIDX stores the non-kept
version and the caller asked for kept objects only. This could be
resolved by teaching the MIDX to resolve duplicates by always favoring
the kept pack (if one exists), but this breaks an assumption in existing
MIDXs, and so it would require a format change.
I don't think this would be possible without a major rethink of how
midxs work. The "keep" property of a pack is not set in stone when the
midx is created. You could add a ".keep" file to one of its packs later,
or even mark one as an in-core keep on the fly. But the duplicate
resolution happens at creation.
So maybe your "breaks an assumption" is the notion that we do not store
duplicate information at all in the midx. If so, then I agree. :) But
I'd also call fixing that more than just a format change.
(None of which changes your point, which isn't that it isn't worth
pursuing that direction).
-Peff
From: Taylor Blau <hidden> Date: 2021-02-16 21:49:06
On Tue, Feb 16, 2021 at 04:42:38PM -0500, Jeff King wrote:
On Wed, Feb 03, 2021 at 10:58:50PM -0500, Taylor Blau wrote:
quoted
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s).
In particular, an new 'git repack' mode which ensures the resulting
Nit (not worth re-rolling): s/an new/a new/
Oops. Good eyes.
quoted
There is a gotcha when looking up objects that are duplicated in kept
and non-kept packs, particularly when the MIDX stores the non-kept
version and the caller asked for kept objects only. This could be
resolved by teaching the MIDX to resolve duplicates by always favoring
the kept pack (if one exists), but this breaks an assumption in existing
MIDXs, and so it would require a format change.
I don't think this would be possible without a major rethink of how
midxs work. The "keep" property of a pack is not set in stone when the
midx is created. You could add a ".keep" file to one of its packs later,
or even mark one as an in-core keep on the fly. But the duplicate
resolution happens at creation.
So maybe your "breaks an assumption" is the notion that we do not store
duplicate information at all in the midx. If so, then I agree. :) But
I'd also call fixing that more than just a format change.
That's part of it, indeed. The part that I was referring to is that
existing MIDX readers expect duplicates to be resolved in a certain way
(effectively in favor of the pack with the lowest mtime). So the easy
part is indicating a format change which tells new readers how to expect
ties to be broken.
But (as you note) that's only part of the problem: even if we say "ties
are resolved in favor of the lowest mtime pack, or a .keep one, if it
exists", then which ones are kept and which aren't? Even *if* we wrote
that down (which I'm not suggesting we do), kept-ness isn't an immutable
property of the pack, and so I think relying on it is a tricky direction
to take.
(None of which changes your point, which isn't that it isn't worth
pursuing that direction).
Yeah; my hope in writing some of this down in the above paragraph is
that it would make clear to future readers that such a MIDX change would
resolve some complexity here, but the complexity it adds in the MIDX
code isn't worth the tradeoff.
From: Jeff King <hidden> Date: 2021-02-16 23:18:38
On Wed, Feb 03, 2021 at 10:58:57PM -0500, Taylor Blau wrote:
quoted hunk
@@ -3797,6 +3807,11 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi return commit_ignore; if (revs->unpacked && has_object_pack(&commit->object.oid)) return commit_ignore;+ if (revs->no_kept_objects) {+ if (has_object_kept_pack(&commit->object.oid,+ revs->keep_pack_cache_flags))+ return commit_ignore;+ }
OK, so this has the same "problems" as --unpacked, which is that we can
miss some objects (i.e., things that are reachable but not-kept may not
be reported). But it should be OK in this version of the series, because
we will not be relying on it for selection of objects, but only to fill
in ordering / namehash fields.
Should we warn people about that, either as a comment or in the commit
message?
+--no-kept-objects[=<kind>]::
+ Halts the traversal as soon as an object in a kept pack is
+ found. If `<kind>` is `on-disk`, only packs with a corresponding
+ `*.keep` file are ignored. If `<kind>` is `in-core`, only packs
+ with their in-core kept state set are ignored. Otherwise, both
+ kinds of kept packs are ignored.
Likewise, I wonder whether we need to expose this mode to users.
Normally I'm a fan of doing so, because it allows scripted callers
access to more of the internals, but:
- the semantics are kind of weird about where we draw the line between
performance and absolute correctness
- the "in-core" thing is a bit weird for callers of rev-list; how do I
as a caller mark a pack as kept-in-core? I think it's only an
internal pack-objects thing.
Once we support this in rev-list, we'll have to do it forever (or deal
with deprecation, etc). If we just need it internally, maybe it's wise
to leave it as a something you ask for by manipulating rev_info
directly. Or perhaps leave it as an undocumented interface we use for
testing, and not something we promise to keep working.
This hunk is interesting.
There is no similar check for revs->unpacked in list-objects.c to cut
off the traversal. And indeed, running "rev-list --unpacked" will
generally look at the _whole_ tree for a commit that is unpacked, even
if all of the tree entries are packed. That's something we might
consider changing in the name of performance (though it does increase
the number of cases where --unpacked will fail to find an unpacked but
reachable object).
But this is a funny place to put it. If I understand it correctly, it is
cutting off the traversal at the very top of the tree. I.e., if we had a
commit that is not-kept, we'd queue it's root tree. And then we might
find that the root tree is kept, and avoid traversing it. But if we _do_
traverse it, we would look at every subtree it contains, even if they
are kept! That's because we recurse the tree via the recursive
process_tree(), not by queueing more objects in the pending array here.
So this check seems to exist in a funny middle ground. I think it's
unlikely to catch anything useful (usually commits have a unique root
tree; it's all of the untouched parts of the subtrees that will be in
the kept packs). IMHO we should either drop it (and act like
"--unpacked", accepting that we may traverse some extra tree objects),
or we should go all-in on performance and cut it off in the top of
process_tree().
-Peff
From: Jeff King <hidden> Date: 2021-02-16 23:48:00
On Wed, Feb 03, 2021 at 10:59:03PM -0500, Taylor Blau wrote:
In an upcoming commit, 'git repack' will want to create a pack comprised
of all of the objects in some packs (the included packs) excluding any
objects in some other packs (the excluded packs).
This caller could iterate those packs themselves and feed the objects it
finds to 'git pack-objects' directly over stdin, but this approach has a
few downsides:
- It requires every caller that wants to drive 'git pack-objects' in
this way to implement pack iteration themselves. This forces the
caller to think about details like what order objects are fed to
pack-objects, which callers would likely rather not do.
- If the set of objects in included packs is large, it requires
sending a lot of data over a pipe, which is inefficient.
- The caller is forced to keep track of the excluded objects, too, and
make sure that it doesn't send any objects that appear in both
included and excluded packs.
But the biggest downside is the lack of a reachability traversal.
Because the caller passes in a list of objects directly, those objects
don't get a namehash assigned to them, which can have a negative impact
on the delta selection process, causing 'git pack-objects' to fail to
find good deltas even when they exist.
The caller could formulate a reachability traversal themselves, but the
only way to drive 'git pack-objects' in this way is to do a full
traversal, and then remove objects in the excluded packs after the
traversal is complete. This can be detrimental to callers who care
about performance, especially in repositories with many objects.
Yep, I think this is a good summary of the problem space, and why this
complexity should be pushed into pack-objects and not the caller.
To address the delta selection problem, 'git pack-objects --stdin-packs'
works as follows. First, it assembles a list of objects that it is going
to pack, as above. Then, a reachability traversal is started, whose tips
are any commits mentioned in included packs. Upon visiting an object, we
find its corresponding object_entry in the to_pack list, and set its
namehash parameter appropriately.
To avoid the traversal visiting more objects than it needs to, the
traversal is halted upon encountering an object which can be found in an
excluded pack (by marking the excluded packs as kept in-core, and
passing --no-kept-objects=in-core to the revision machinery).
This can cause the traversal to halt early, for example if an object in
an included pack is an ancestor of ones in excluded packs. But stopping
early is OK, since filling in the namehash fields of objects in the
to_pack list is only additive (i.e., having it helps the delta selection
process, but leaving it blank doesn't impact the correctness of the
resulting pack).
OK, good. Definitely worth calling out this subtle distinction of
correctness versus the heuristic.
Do we use this partial traversal to impact the write order at all? That
would be a nice-to-have, but I suspect that just concatenating the packs
(presumably by descending mtime) ends up with a similar result.
@@ -85,6 +85,16 @@ base-name:: reference was included in the resulting packfile. This can be useful to send new tags to native Git clients.+--stdin-packs::+ Read the basenames of packfiles from the standard input, instead+ of object names or revision arguments. The resulting pack+ contains all objects listed in the included packs (those not+ beginning with `^`), excluding any objects listed in the+ excluded packs (beginning with `^`).+++Incompatible with `--revs`, or options that imply `--revs` (such as+`--all`), with the exception of `--unpacked`, which is compatible.
I know you say "basename" here, but I wonder if it is worth giving an
example (`pack-1234abcd.pack`) to make it clear in what form we expect
it. Or possibly something in the `EXAMPLES` section.
I scratched my head at these until I looked further in the code. They're
the counters for the trace output. Might be worth a brief comment above
them. (I do approve of adding this kind of trace debugging info; I'm
pretty accustomed to using gdb or adding one-off debug statements, but
we really could do a better job in general of making these kinds of
internals visible to mere mortal admins).
+static int add_object_entry_from_pack(const struct object_id *oid,
+ struct packed_git *p,
+ uint32_t pos,
+ void *_data)
+{
+ struct rev_info *revs = _data;
+ struct object_info oi = OBJECT_INFO_INIT;
+ off_t ofs;
+ enum object_type type;
+
+ display_progress(progress_state, ++nr_seen);
+
+ ofs = nth_packed_object_offset(p, pos);
+
+ oi.typep = &type;
+ if (packed_object_info(the_repository, p, ofs, &oi) < 0)
+ die(_("could not get type of object %s in pack %s"),
+ oid_to_hex(oid), p->pack_name);
Calling out for other reviewers: the oi.typep field will be filled in
the with _real_ type of the object, even if it's a delta. This is as
opposed to the return value of packed_object_info(), which may be
OFS_DELTA or REF_DELTA.
And that real type is what we want here:
+ else if (type == OBJ_COMMIT) {
+ /*
+ * commits in included packs are used as starting points for the
+ * subsequent revision walk
+ */
+ add_pending_oid(revs, NULL, oid, 0);
+ }
And later when we call create_object_entry().
I wondered whether it would be worth adding other objects we might find,
like trees, in order to increase our traversal. But that doesn't make
any sense. The whole point is to find the paths, which come from
traversing from the root trees. And we can only find the root trees by
starting at commits. Adding any random tree we found would defeat the
purpose (most of them are sub-trees and would give us a useless partial
path).
Should we avoid adding the commit as a tip for walking if it won't end
up in the resulting pack? I.e., should we check these:
+ if (have_duplicate_entry(oid, 0))
+ return 0;
+
+ if (!want_object_in_pack(oid, 0, &p, &ofs))
+ return 0;
...first? I guess it probably doesn't matter too much since we'd
truncate the traversal as soon as we saw it was in a kept pack anyway.
Nothing to do here, since commits don't have a name field. Makes sense.
+static void show_object_pack_hint(struct object *object, const char *name,
+ void *_data)
+{
+ struct object_entry *oe = packlist_find(&to_pack, &object->oid);
+ if (!oe)
+ return;
+
+ /*
+ * Our 'to_pack' list was constructed by iterating all objects packed in
+ * included packs, and so doesn't have a non-zero hash field that you
+ * would typically pick up during a reachability traversal.
+ *
+ * Make a best-effort attempt to fill in the ->hash and ->no_try_delta
+ * here using a now in order to perhaps improve the delta selection
+ * process.
+ */
+ oe->hash = pack_name_hash(name);
+ oe->no_try_delta = name && no_try_delta(name);
+
+ stdin_packs_hints_nr++;
+}
But for actual objects, we do fill in the hash. I wonder if it's
possible for oe->hash to have been already filled. I don't think it
really matters, though. Any value we get is equally valid, so
overwriting is OK in that case.
OK, here we're just filling in the util field with each found pack. So
we wouldn't notice a pack that we didn't find, but we will in the
subsequent loops. Makes sense.
I think you could do without string lists at all by using the recent-ish
pack-hash to efficiently look up the names, but I'm perfectly content to
see it all handled within this function.
+ /*
+ * First handle all of the excluded packs, marking them as kept in-core
+ * so that later calls to add_object_entry() discards any objects that
+ * are also found in excluded packs.
+ */
+ for_each_string_list_item(item, &exclude_packs) {
+ struct packed_git *p = item->util;
+ if (!p)
+ die(_("could not find pack '%s'"), item->string);
+ p->pack_keep_in_core = 1;
+ }
+ for_each_string_list_item(item, &include_packs) {
+ struct packed_git *p = item->util;
+ if (!p)
+ die(_("could not find pack '%s'"), item->string);
+ for_each_object_in_pack(p,
+ add_object_entry_from_pack,
+ &revs,
+ FOR_EACH_OBJECT_PACK_ORDER);
+ }
Yeah, this ordering makes sense.
+ if (prepare_revision_walk(&revs))
+ die(_("revision walk setup failed"));
+ traverse_commit_list(&revs,
+ show_commit_pack_hint,
+ show_object_pack_hint,
+ NULL);
And this traversal is pretty straight-forward. Looks good.
I wonder if it makes sense to report the actual set of packs via trace
(obviously not as an int, but as a list). That's less helpful for
debugging pack-objects, if you just fed it the input anyway, but if you
were debugging "git repack --geometric" it might be useful to see which
packs it thought were which (though arguably that would be a useful
trace in builtin/repack.c instead).
OK, this is necessary to avoid triggering the internal rev-list, because
we handle --unpacked ourselves specially later here...
quoted hunk
@@ -3741,7 +3907,13 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix) if (progress) progress_state = start_progress(_("Enumerating objects"), 0);- if (!use_internal_rev_list)+ if (stdin_packs) {+ /* avoids adding objects in excluded packs */+ ignore_packed_keep_in_core = 1;+ read_packs_list_from_stdin();+ if (rev_list_unpacked)+ add_unreachable_loose_objects();
Which isn't quite behaving like normal --unpacked (in that we are adding
all loose objects, not just reachable ones). I think we actually could
just add --unpacked as part of our heuristic traversal. It's not
perfect, but unlike the packed objects, it's OK for us to miss some
corner cases (they just end up not getting packed; they don't get
deleted).
I'm OK to consider that an implementation detail for now, though. We can
change it later without impacting the interface.
+ if (rev_list_unpacked)
+ add_unreachable_loose_objects();
Despite the name, that function is adding both reachable and unreachable
ones. So it is doing what you want. It might be worth renaming, but it's
not too big a deal since it's local to this file.
-Peff
From: Jeff King <hidden> Date: 2021-02-16 23:59:01
On Wed, Feb 03, 2021 at 10:59:13PM -0500, Taylor Blau wrote:
From: Jeff King <redacted>
This is the same as the regular repack test, except that we mark the
single base pack as "kept" and use --assume-kept-packs-closed. The
I don't think that option exists anymore. I guess we are just using
--stdin-packs, which causes us to mark a pack as kept.
I think we could just mark it in the filesystem and use
--honor-pack-keep, which would make it independent of your new feature.
At first I was going to say "but it doesn't matter either way", but...
theory is that this should be faster than the normal repack, because
we'll have fewer objects to traverse and process.
Here are some timings on a recent clone of the kernel. In the
single-pack case, there is nothing do since there are no non-excluded
packs:
5303.5: repack (1) 57.42(54.88+10.64)
5303.6: repack with --stdin-packs (1) 0.01(0.01+0.00)
and in the 50-pack case, it is much faster to use `--stdin-packs`, since
we avoid having to consider any objects in the excluded pack:
5303.10: repack (50) 71.26(88.24+4.96)
5303.11: repack with --stdin-packs (50) 3.49(11.82+0.28)
but our improvements vanish as we approach 1000 packs.
5303.15: repack (1000) 215.64(491.33+14.80)
5303.16: repack with --stdin-packs (1000) 198.79(380.51+7.97)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Well, part of it is just that with 1000 packs we have 20 times as many
objects that are actually getting packed with --stdin-packs, compared to
the 50-pack case. IIRC, each pack is a fixed-size slice and then the
residual is put into the .keep pack. So the fact that the time gets
closer to a full repack as we add more packs is expected: we are asking
pack-objects to do more work!
For showing the impact of the optimizations in patches 7 and 8, I think
doing a full repack with --honor-pack-keep is a better test. Because
then we're always doing a full traversal, and most of the work continues
to scale with the repo size (though obviously not the actual shuffling
of packed bytes around). That would get rid of the weird "no work to do"
case in the single-pack tests, too.
-Peff
From: Jeff King <hidden> Date: 2021-02-17 00:01:56
On Wed, Feb 03, 2021 at 10:58:45PM -0500, Taylor Blau wrote:
The details of the new approach can be found in the third patch, but the gist is
as follows:
[...]
I think this turned out very nice (and less complicated than I feared it
might). I've read up through patch 5. I think the overall approach is
good, but I had various small-to-medium comments.
I'll try to pick up reviewing the rest tomorrow, though it may make
sense to resolve the earlier comments first.
-Peff
From: Jeff King <hidden> Date: 2021-02-17 00:03:38
On Tue, Feb 16, 2021 at 06:58:16PM -0500, Jeff King wrote:
For showing the impact of the optimizations in patches 7 and 8, I think
doing a full repack with --honor-pack-keep is a better test. Because
then we're always doing a full traversal, and most of the work continues
to scale with the repo size (though obviously not the actual shuffling
of packed bytes around). That would get rid of the weird "no work to do"
case in the single-pack tests, too.
I meant to add: but I do like that we are timing --stdin-packs, too. We
may actually want to time both.
Another thing we _could_ do, if we have --honor-pack-keep perf tests, is
to shuffle patches 5, 6, and 7 towards the front of the series. They
should be able to show off the improvement even without the
--stdin-packs feature.
-Peff
From: Jeff King <hidden> Date: 2021-02-17 16:06:22
On Wed, Feb 03, 2021 at 10:59:17PM -0500, Taylor Blau wrote:
quoted hunk
@@ -1209,22 +1210,73 @@ static int want_found_object(int exclude, struct packed_git *p) * Otherwise, we signal "-1" at the end to tell the caller that we do * not know either way, and it needs to check more packs. */- if (!ignore_packed_keep_on_disk &&- !ignore_packed_keep_in_core &&- (!local || !have_non_local_packs))++ /*+ * Handle .keep first, as we have a fast(er) path there.+ */+ if (ignore_packed_keep_on_disk || ignore_packed_keep_in_core) {+ /*+ * Set the flags for the kept-pack cache to be the ones we want+ * to ignore.+ *+ * That is, if we are ignoring objects in on-disk keep packs,+ * then we want to search through the on-disk keep and ignore+ * the in-core ones.+ */+ unsigned flags = 0;+ if (ignore_packed_keep_on_disk)+ flags |= ON_DISK_KEEP_PACKS;+ if (ignore_packed_keep_in_core)+ flags |= IN_CORE_KEEP_PACKS;++ if (ignore_packed_keep_on_disk && p->pack_keep)+ return 0;+ if (ignore_packed_keep_in_core && p->pack_keep_in_core)+ return 0;+ if (has_object_kept_pack(oid, flags))+ return 0;+ }++ /*+ * At this point we know definitively that either we don't care about+ * keep-packs, or the object is not in one. Keep checking other+ * conditions...+ */++ if (!local || !have_non_local_packs) return 1;- if (local && !p->pack_local) return 0;- if (p->pack_local &&- ((ignore_packed_keep_on_disk && p->pack_keep) ||- (ignore_packed_keep_in_core && p->pack_keep_in_core)))- return 0; /* we don't know yet; keep looking for more packs */ return -1;
I know I wrote this patch, but just looking it over again with a
critical eye: it looks like more re-ordering could avoid work in some
cases.
In particular, has_object_kept_pack() is a potentially expensive call.
But if "(local && !p->pack_local)" is true, then we could cheaply exit
the function with "0", regardless of what the keep requirement says.
That's not a case that I think anybody cares that deeply about (and it
certainly is not covered by t/perf). But I think it does regress in this
patch. Prior to the patch, we'd check that condition before returning
-1, and it was the caller who would then continue to search through all
the kept packs. Now we do it preemptively.
I think just bumping that:
if (local && !p->pack_local)
return 0;
above the new code would fix it. Or to lay out the logic more fully, the
order of checks should be:
- does _this_ pack we found the object in disqualify it. If so, we can
cheaply return 0. And that applies to both keep and local rules.
- otherwise, check all packs via has_object_kept_pack(), which is
cheaper than continuing to iterate through all packs by returning
-1.
- once we know definitively about keep-packs, then check any shortcuts
related to local packs (like !have_non_local_packs)
- and then if no shortcuts, we return -1
I think that might be easier to express by rewriting the patch. :)
-Peff
@@ -1225,9 +1225,9 @@ static int want_found_object(const struct object_id *oid, int exclude,*/unsignedflags=0;if(ignore_packed_keep_on_disk)-flags|=ON_DISK_KEEP_PACKS;+flags|=CACHE_ON_DISK_KEEP_PACKS;if(ignore_packed_keep_in_core)-flags|=IN_CORE_KEEP_PACKS;+flags|=CACHE_IN_CORE_KEEP_PACKS;
Why are we renaming the constants in this patch?
I know I'm listed as the author, but I think this came out of some
off-list back and forth between us. It seems like the existing constants
would have been fine.
OK, so we keep a single cache based on the flags, and then if somebody
ever asks for different flags, we throw it away. That's probably OK for
our purposes, since we wouldn't expect multiple callers within a single
process.
I wondered if it would be simpler to just keep two lists, one for
in-core keeps and one for on-disk keeps. And then just walk over each
list separately based on the query flags. That makes things more robust
_and_ I think would be less code. It does mean that a pack could appear
in both lists, though, which means we might do a lookup in it twice.
That doesn't seem all that likely, but it is working against our goal
here.
Another option is to keep 3 caches (two separate and one combined),
rather than flipping between them. I'm not sure if that would be less
code or not (it gets rid of the "invalidate" function, but you do have
to pick the right cache depending on the query flags).
Yet another option is to keep a cache of any that are marked as _either_
in core or on-disk keeps, and then decide to look up the object based on
the query flags. Then you just pay the cost to iterate over the list and
check the flags (which really is all this cache is helping with in the
first place).
I dunno. TBH, I kind of wonder if this whole patch is worth doing at
all, giving the underwhelming performance benefit (3% on the
pathological 1000-pack case). When I had timed this strategy initially,
it was more like 15%. I'm not sure where the savings went in the
interim, or if it was a timing fluke.
+static struct packed_git **kept_pack_cache(struct repository *r, unsigned flags)
+{
+ maybe_invalidate_kept_pack_cache(r, flags);
+
+ if (!r->objects->kept_pack_cache) {
+ struct packed_git **packs = NULL;
+ size_t nr = 0, alloc = 0;
+ struct packed_git *p;
+
+ /*
+ * We want "all" packs here, because we need to cover ones that
+ * are used by a midx, as well. We need to look in every one of
+ * them (instead of the midx itself) to cover duplicates. It's
+ * possible that an object is found in two packs that the midx
+ * covers, one kept and one not kept, but the midx returns only
+ * the non-kept version.
+ */
+ for (p = get_all_packs(r); p; p = p->next) {
+ if ((p->pack_keep && (flags & CACHE_ON_DISK_KEEP_PACKS)) ||
+ (p->pack_keep_in_core && (flags & CACHE_IN_CORE_KEEP_PACKS))) {
+ ALLOC_GROW(packs, nr + 1, alloc);
+ packs[nr++] = p;
+ }
+ }
+ ALLOC_GROW(packs, nr + 1, alloc);
+ packs[nr] = NULL;
+
+ r->objects->kept_pack_cache = xmalloc(sizeof(*r->objects->kept_pack_cache));
+ r->objects->kept_pack_cache->packs = packs;
+ r->objects->kept_pack_cache->flags = flags;
+ }
Is there any reason not to just embed the kept_pack_cache struct inside
the object_store? It's one less pointer to deal with. I wonder if this
is a holdover from an attempt to have multiple caches.
(I also think it would be reasonable if we wanted to hide the definition
of the cache struct from callers, but we don't seem do to that).
I notice that when the constants moved, we didn't keep an equivalent of
ALL_KEEP_PACKS. Maybe we didn't need it in the first place in patch 1?
BTW, I absolutely hate the complication that all of this on-disk
versus in-core keep distinction brings to this code. And I wondered
what it was really doing for us and whether we could get rid of it.
But I think we do need it: a common case may be to avoid using
--honor-pack-keep (because you don't want to deal with racy .keep
writes from incoming receive-pack processes), but use in-core ones for
something like --stdin-packs. So we do need to respect one and not the
other.
I do wonder if things would be simpler if pack-objects simply kept its
own list of "in core" packs in a separate array. But that is really
just another form of the same problem, I guess.
-Peff
From: Jeff King <hidden> Date: 2021-02-17 18:18:21
On Wed, Feb 03, 2021 at 10:59:25PM -0500, Taylor Blau wrote:
Often it is useful to both:
- have relatively few packfiles in a repository, and
- avoid having so few packfiles in a repository that we repack its
entire contents regularly
This patch implements a '--geometric=<n>' option in 'git repack'. This
allows the caller to specify that they would like each pack to be at
least a factor times as large as the previous largest pack (by object
count).
Concretely, say that a repository has 'n' packfiles, labeled P1, P2,
..., up to Pn. Each packfile has an object count equal to 'objects(Pn)'.
With a geometric factor of 'r', it should be that:
objects(Pi) > r*objects(P(i-1))
for all i in [1, n], where the packs are sorted by
objects(P1) <= objects(P2) <= ... <= objects(Pn).
Just devil's advocating for a moment.
I think in this kind of geometric roll-up strategy, you want to imagine
that you are rolling up recent pushes but leaving untouched a good
"base" pack that you previously created.
And that will usually be true if you are doing the rollup based on
number of objects (or size, etc). But it won't always be (e.g., for some
reason somebody makes a very large push relative to the current
repository size). What happens when this assumption is violated?
In some ways, it is a good thing to drift away from this "base pack"
view of the world. If we're trying to amortize the per-object work done,
then we are better off rolling up the small things into the large,
regardless of where they came from.
But the base pack may also have other properties we want to retain. Two
I can think of:
- it may have a .bitmap that we'll be throwing away, without
generating a new one. I know that your end-game involves writing a
midx with bitmaps that covers all of the packs, so this would become
a non-issue in that strategy.
- it may have been more carefully packed (e.g., with a larger window
size, using "-f", etc) than the packs we got from pushes. We do
_mostly_ retain the deltas when we roll up the packs, so it probably
only has a small impact in practice (I'd expect in a few cases we'd
throw away deltas because a pushed pack contains a duplicate of its
base object that we added via --fix-thin).
So I suspect it's probably OK in practice. These cases would happen
rarely, and the impact would not be all that big. The bitmap thing I'd
worry the most about. As part of a larger strategy involving a midx it
is taken care of, but people using just this new feature may not realize
that. The bitmaps of course are "just" an optimization, but it's hard to
say how dire things are when they don't exist. For many situations,
probably not very dire. But I know that on our servers, when repos lack
bitmaps, people notice the performance degradation.
On the other hand, by definition this happens in a case where there are
more objects that have just been pushed (and are therefore not
bitmapped) than existed already. So you _already_ have a performance
problem either way until you get bitmap coverage of those new objects.
@@ -165,6 +165,17 @@ depth is 4095. Pass the `--delta-islands` option to `git-pack-objects`, see linkgit:git-pack-objects[1].+-g=<factor>::+--geometric=<factor>::+ Arrange resulting pack structure so that each successive pack+ contains at least `<factor>` times the number of objects as the+ next-largest pack.+++`git repack` ensures this by determining a "cut" of packfiles that need to be+repacked into one in order to ensure a geometric progression. It picks the+smallest set of packfiles such that as many of the larger packfiles (by count of+objects contained in that pack) may be left intact.
I think we might need to make clear in the documentation how this
differs from other repacks, in that it is not considering reachability
at all. I like the term "roll up" to describe what is happening, but we
probably need to define that term clearly, as well.
Especially important, I think, is that we talk about what's happening
with loose objects, which are part of the rollup here. And IMHO we
should make clear that for now we include them all, without
consideration of their reachability, but that this may change in the
future.
Likewise, are there any options that are incompatible with "-g"? I have
to imagine that "--write-bitmap-index" would not work very well. I don't
know that we need to enumerate them all, but I'm wondering if a blanket
"this may not play well with other options" warning may be advisable.
+static void split_pack_geometry(struct pack_geometry *geometry, int factor)
[...]
I'll admit I didn't carefully think about the math of the progression
here. IMHO the exact split is the least interesting part of this whole
series (compared to the general idea of "rolling up some packs" versus a
whole repack). Between the comments and the tests, I'll assume it's
generally behaving as advertised. (I of course did look for any obvious
coding errors, but didn't see any).
-Peff
From: Jeff King <hidden> Date: 2021-02-17 18:19:39
On Tue, Feb 16, 2021 at 07:01:13PM -0500, Jeff King wrote:
On Wed, Feb 03, 2021 at 10:58:45PM -0500, Taylor Blau wrote:
quoted
The details of the new approach can be found in the third patch, but the gist is
as follows:
[...]
I think this turned out very nice (and less complicated than I feared it
might). I've read up through patch 5. I think the overall approach is
good, but I had various small-to-medium comments.
I'll try to pick up reviewing the rest tomorrow, though it may make
sense to resolve the earlier comments first.
OK, I finished reading the rest and left a few more comments. The short
of it is that I really like the new direction, but I think there are
enough small comments to merit a re-roll, which I hope would probably be
the final.
-Peff
From: Taylor Blau <hidden> Date: 2021-02-17 18:36:51
On Tue, Feb 16, 2021 at 06:17:40PM -0500, Jeff King wrote:
On Wed, Feb 03, 2021 at 10:58:57PM -0500, Taylor Blau wrote:
quoted
@@ -3797,6 +3807,11 @@ enum commit_action get_commit_action(struct rev_info *revs, struct commit *commi return commit_ignore; if (revs->unpacked && has_object_pack(&commit->object.oid)) return commit_ignore;+ if (revs->no_kept_objects) {+ if (has_object_kept_pack(&commit->object.oid,+ revs->keep_pack_cache_flags))+ return commit_ignore;+ }
OK, so this has the same "problems" as --unpacked, which is that we can
miss some objects (i.e., things that are reachable but not-kept may not
be reported). But it should be OK in this version of the series, because
we will not be relying on it for selection of objects, but only to fill
in ordering / namehash fields.
Should we warn people about that, either as a comment or in the commit
message?
Yeah, let's warn about it in the commit message. We could put it in the
documentation, but...
quoted
+--no-kept-objects[=<kind>]::
+ Halts the traversal as soon as an object in a kept pack is
+ found. If `<kind>` is `on-disk`, only packs with a corresponding
+ `*.keep` file are ignored. If `<kind>` is `in-core`, only packs
+ with their in-core kept state set are ignored. Otherwise, both
+ kinds of kept packs are ignored.
Likewise, I wonder whether we need to expose this mode to users.
Normally I'm a fan of doing so, because it allows scripted callers
access to more of the internals, but:
- the semantics are kind of weird about where we draw the line between
performance and absolute correctness
- the "in-core" thing is a bit weird for callers of rev-list; how do I
as a caller mark a pack as kept-in-core? I think it's only an
internal pack-objects thing.
Once we support this in rev-list, we'll have to do it forever (or deal
with deprecation, etc). If we just need it internally, maybe it's wise
to leave it as a something you ask for by manipulating rev_info
directly. Or perhaps leave it as an undocumented interface we use for
testing, and not something we promise to keep working.
I think that you raise a good point about not advertising this option,
since doing so paints us into a corner that we have to keep it working
and behaving consistently forever.
I'm not opposed to the idea that we may eventually want to do so, but I
think that this is too early for that. As you note, we *could* just
expose it in rev_info flags, but that makes it much more difficult to
test some of the tricky cases that are added in t6114, so I think a
middle ground of having an undocumented option satisfies both of our
wants.
This hunk is interesting.
There is no similar check for revs->unpacked in list-objects.c to cut
off the traversal. And indeed, running "rev-list --unpacked" will
generally look at the _whole_ tree for a commit that is unpacked, even
if all of the tree entries are packed. That's something we might
consider changing in the name of performance (though it does increase
the number of cases where --unpacked will fail to find an unpacked but
reachable object).
But this is a funny place to put it. If I understand it correctly, it is
cutting off the traversal at the very top of the tree. I.e., if we had a
commit that is not-kept, we'd queue it's root tree. And then we might
find that the root tree is kept, and avoid traversing it. But if we _do_
traverse it, we would look at every subtree it contains, even if they
are kept! That's because we recurse the tree via the recursive
process_tree(), not by queueing more objects in the pending array here.
So this check seems to exist in a funny middle ground. I think it's
unlikely to catch anything useful (usually commits have a unique root
tree; it's all of the untouched parts of the subtrees that will be in
the kept packs). IMHO we should either drop it (and act like
"--unpacked", accepting that we may traverse some extra tree objects),
or we should go all-in on performance and cut it off in the top of
process_tree().
From: Taylor Blau <hidden> Date: 2021-02-17 19:00:10
On Tue, Feb 16, 2021 at 06:46:59PM -0500, Jeff King wrote:
Do we use this partial traversal to impact the write order at all? That
would be a nice-to-have, but I suspect that just concatenating the packs
(presumably by descending mtime) ends up with a similar result.
We don't; the objects are written in pack order. In the version of the
patch you reviewed, the order of packs was determined by their hash (due
to the string_list_sort()), but the version I just prepared re-sorts by
mtime.
It's kind of gross, since we need to use QSORT directly on the
string_list internals in order to have access to the ->util field of the
string_list_items (string_list_sort() only lets you compare strings
directly for obvious reasons).
I added a comment describing this hack.
quoted
+--stdin-packs::
+ Read the basenames of packfiles from the standard input, instead
+ of object names or revision arguments. The resulting pack
+ contains all objects listed in the included packs (those not
+ beginning with `^`), excluding any objects listed in the
+ excluded packs (beginning with `^`).
++
+Incompatible with `--revs`, or options that imply `--revs` (such as
+`--all`), with the exception of `--unpacked`, which is compatible.
I know you say "basename" here, but I wonder if it is worth giving an
example (`pack-1234abcd.pack`) to make it clear in what form we expect
it. Or possibly something in the `EXAMPLES` section.
I scratched my head at these until I looked further in the code. They're
the counters for the trace output. Might be worth a brief comment above
them. (I do approve of adding this kind of trace debugging info; I'm
pretty accustomed to using gdb or adding one-off debug statements, but
we really could do a better job in general of making these kinds of
internals visible to mere mortal admins).
Good call.
quoted
+static int add_object_entry_from_pack(const struct object_id *oid,
+ struct packed_git *p,
+ uint32_t pos,
+ void *_data)
+{
+ struct rev_info *revs = _data;
+ struct object_info oi = OBJECT_INFO_INIT;
+ off_t ofs;
+ enum object_type type;
+
+ display_progress(progress_state, ++nr_seen);
+
+ ofs = nth_packed_object_offset(p, pos);
+
+ oi.typep = &type;
+ if (packed_object_info(the_repository, p, ofs, &oi) < 0)
+ die(_("could not get type of object %s in pack %s"),
+ oid_to_hex(oid), p->pack_name);
Calling out for other reviewers: the oi.typep field will be filled in
the with _real_ type of the object, even if it's a delta. This is as
opposed to the return value of packed_object_info(), which may be
OFS_DELTA or REF_DELTA.
And that real type is what we want here:
quoted
+ else if (type == OBJ_COMMIT) {
+ /*
+ * commits in included packs are used as starting points for the
+ * subsequent revision walk
+ */
+ add_pending_oid(revs, NULL, oid, 0);
+ }
And later when we call create_object_entry().
:-). Yes indeed. As I'm sure that you will recall, the pack-objects
code _does not_ behave well when you give it the packed type of an
object (which is not entirely unexpected, since the pack-objects code
only operates on the true type, so passing the packed type--as I did
when originally writing this patch--is a bug).
I wondered whether it would be worth adding other objects we might find,
like trees, in order to increase our traversal. But that doesn't make
any sense. The whole point is to find the paths, which come from
traversing from the root trees. And we can only find the root trees by
starting at commits. Adding any random tree we found would defeat the
purpose (most of them are sub-trees and would give us a useless partial
path).
Right.
Should we avoid adding the commit as a tip for walking if it won't end
up in the resulting pack? I.e., should we check these:
quoted
+ if (have_duplicate_entry(oid, 0))
+ return 0;
+
+ if (!want_object_in_pack(oid, 0, &p, &ofs))
+ return 0;
...first? I guess it probably doesn't matter too much since we'd
truncate the traversal as soon as we saw it was in a kept pack anyway.
I agree it doesn't make a difference, but I think placing the extra
guards first makes it easier to read (since the reader doesn't have to
consider how the subsequent traversal would treat it).
Nothing to do here, since commits don't have a name field. Makes sense.
Yeah. I added a comment to say the same thing, just for extra clarity.
quoted
+static void show_object_pack_hint(struct object *object, const char *name,
+ void *_data)
+{
+ struct object_entry *oe = packlist_find(&to_pack, &object->oid);
+ if (!oe)
+ return;
+
+ /*
+ * Our 'to_pack' list was constructed by iterating all objects packed in
+ * included packs, and so doesn't have a non-zero hash field that you
+ * would typically pick up during a reachability traversal.
+ *
+ * Make a best-effort attempt to fill in the ->hash and ->no_try_delta
+ * here using a now in order to perhaps improve the delta selection
+ * process.
+ */
+ oe->hash = pack_name_hash(name);
+ oe->no_try_delta = name && no_try_delta(name);
+
+ stdin_packs_hints_nr++;
+}
But for actual objects, we do fill in the hash. I wonder if it's
possible for oe->hash to have been already filled. I don't think it
really matters, though. Any value we get is equally valid, so
overwriting is OK in that case.
I wonder if it makes sense to report the actual set of packs via trace
(obviously not as an int, but as a list). That's less helpful for
debugging pack-objects, if you just fed it the input anyway, but if you
were debugging "git repack --geometric" it might be useful to see which
packs it thought were which (though arguably that would be a useful
trace in builtin/repack.c instead).
I could see an argument in both ways. I'd rather pass for now until we
have a clearer need for it.
[passing --unpacked to the namehash traversal]
I'm OK to consider that an implementation detail for now, though. We can
change it later without impacting the interface.
Agreed.
quoted
+ if (rev_list_unpacked)
+ add_unreachable_loose_objects();
Despite the name, that function is adding both reachable and unreachable
ones. So it is doing what you want. It might be worth renaming, but it's
not too big a deal since it's local to this file.
Yeah, I tend to err on the side of "it's fine as-is" since this isn't
exposed outside of pack-objects internals. If you feel strongly I'm
happy to change it, but I suspect you don't.
Thanks,
Taylor
From: Taylor Blau <hidden> Date: 2021-02-17 19:14:50
On Tue, Feb 16, 2021 at 06:58:16PM -0500, Jeff King wrote:
On Wed, Feb 03, 2021 at 10:59:13PM -0500, Taylor Blau wrote:
quoted
From: Jeff King <redacted>
This is the same as the regular repack test, except that we mark the
single base pack as "kept" and use --assume-kept-packs-closed. The
I don't think that option exists anymore. I guess we are just using
--stdin-packs, which causes us to mark a pack as kept.
I think we could just mark it in the filesystem and use
--honor-pack-keep, which would make it independent of your new feature.
At first I was going to say "but it doesn't matter either way", but...
quoted
theory is that this should be faster than the normal repack, because
we'll have fewer objects to traverse and process.
Here are some timings on a recent clone of the kernel. In the
single-pack case, there is nothing do since there are no non-excluded
packs:
5303.5: repack (1) 57.42(54.88+10.64)
5303.6: repack with --stdin-packs (1) 0.01(0.01+0.00)
and in the 50-pack case, it is much faster to use `--stdin-packs`, since
we avoid having to consider any objects in the excluded pack:
5303.10: repack (50) 71.26(88.24+4.96)
5303.11: repack with --stdin-packs (50) 3.49(11.82+0.28)
but our improvements vanish as we approach 1000 packs.
5303.15: repack (1000) 215.64(491.33+14.80)
5303.16: repack with --stdin-packs (1000) 198.79(380.51+7.97)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Well, part of it is just that with 1000 packs we have 20 times as many
objects that are actually getting packed with --stdin-packs, compared to
the 50-pack case. IIRC, each pack is a fixed-size slice and then the
residual is put into the .keep pack. So the fact that the time gets
closer to a full repack as we add more packs is expected: we are asking
pack-objects to do more work!
No, the residual base pack isn't marked as kept on-disk. But the
--stdin-packs test treats it as such, by passing '^pack-$base_pack.pack'
as input to '--stdin-packs' (thus marking it as kept in-core).
For showing the impact of the optimizations in patches 7 and 8, I think
doing a full repack with --honor-pack-keep is a better test. Because
then we're always doing a full traversal, and most of the work continues
to scale with the repo size (though obviously not the actual shuffling
of packed bytes around). That would get rid of the weird "no work to do"
case in the single-pack tests, too.
I think you're suggesting that we change the "repack ($nr_packs)" test
to have the residual pack marked as kept (so we're measuring time it
takes to repack everything that _isn't_ in the base pack)?
That would allow a more direct comparison, but I think it's loosing out
on an important aspect which is how long it takes to pack the entire
repository. Maybe we want three.
What do you think?
From: Jeff King <hidden> Date: 2021-02-17 19:22:16
On Wed, Feb 17, 2021 at 01:59:08PM -0500, Taylor Blau wrote:
quoted
quoted
+ if (rev_list_unpacked)
+ add_unreachable_loose_objects();
Despite the name, that function is adding both reachable and unreachable
ones. So it is doing what you want. It might be worth renaming, but it's
not too big a deal since it's local to this file.
Yeah, I tend to err on the side of "it's fine as-is" since this isn't
exposed outside of pack-objects internals. If you feel strongly I'm
happy to change it, but I suspect you don't.
Yeah, I don't feel strongly (and if we did change it, it should be in a
separate patch anyway).
-Peff
From: Taylor Blau <hidden> Date: 2021-02-17 19:24:15
On Wed, Feb 17, 2021 at 11:05:22AM -0500, Jeff King wrote:
I think just bumping that:
if (local && !p->pack_local)
return 0;
above the new code would fix it. Or to lay out the logic more fully, the
order of checks should be:
- does _this_ pack we found the object in disqualify it. If so, we can
cheaply return 0. And that applies to both keep and local rules.
- otherwise, check all packs via has_object_kept_pack(), which is
cheaper than continuing to iterate through all packs by returning
-1.
- once we know definitively about keep-packs, then check any shortcuts
related to local packs (like !have_non_local_packs)
- and then if no shortcuts, we return -1
I don't understand what you're suggesting. Is the (local &&
!p->pack_local) a disqualifying condition? Reading the comment, I think
it is, and so we could do something like:
@@ -1205,14 +1205,21 @@ static int want_found_object(const struct object_id *oid, int exclude,*makesurenocopyofthisobjectappearsin_any_packthatmakesus*toomittheobject,soweneedtocheckallthepacks.*-*Wecanhoweverfirstcheckwhethertheseoptionscanpossiblematter;+*Wecanhoweverfirstcheckwhethertheseoptionscanpossiblymatter;*iftheydonotmatterweknowwewanttheobjectingeneratedpack.*Otherwise,wesignal"-1"attheendtotellthecallerthatwedo*notknoweitherway,anditneedstocheckmorepacks.*//*-*Handle.keepfirst,aswehaveafast(er)paththere.+*Objectsinpacksborrowedfromelsewherearediscardedregardlessof+*iftheyappearinotherpacksthatweren'tborrowed.+*/+if(local&&!p->pack_local)+return0;++/*+*Thenhandle.keepfirst,aswehaveafast(er)paththere.*/if(ignore_packed_keep_on_disk||ignore_packed_keep_in_core){/*
@@ -1242,11 +1249,8 @@ static int want_found_object(const struct object_id *oid, int exclude,*keep-packs,ortheobjectisnotinone.Keepcheckingother*conditions...*/-if(!local||!have_non_local_packs)return1;-if(local&&!p->pack_local)-return0;/* we don't know yet; keep looking for more packs */return-1;
But your "check any shortcuts related to local packs" makes me think
that we should leave the code as-is.
Which are you suggesting?
Thanks,
Taylor
From: Jeff King <hidden> Date: 2021-02-17 19:26:35
On Wed, Feb 17, 2021 at 02:13:47PM -0500, Taylor Blau wrote:
quoted
quoted
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Well, part of it is just that with 1000 packs we have 20 times as many
objects that are actually getting packed with --stdin-packs, compared to
the 50-pack case. IIRC, each pack is a fixed-size slice and then the
residual is put into the .keep pack. So the fact that the time gets
closer to a full repack as we add more packs is expected: we are asking
pack-objects to do more work!
No, the residual base pack isn't marked as kept on-disk. But the
--stdin-packs test treats it as such, by passing '^pack-$base_pack.pack'
as input to '--stdin-packs' (thus marking it as kept in-core).
Sorry, I perhaps shouldn't have said ".keep" here. But it's the same
thing, isn't it? The 50 pack case is packing 50*pack_size objects
(because it's excluding everything else that is in the base pack we mark
as keep-in-core), and the 1000-pack case is packing 1000*pack_size
objects (for the same reason).
So any patterns we see between them have more to do with that, than how
the keep-handling code scales with the number of non-kept packs.
quoted
For showing the impact of the optimizations in patches 7 and 8, I think
doing a full repack with --honor-pack-keep is a better test. Because
then we're always doing a full traversal, and most of the work continues
to scale with the repo size (though obviously not the actual shuffling
of packed bytes around). That would get rid of the weird "no work to do"
case in the single-pack tests, too.
I think you're suggesting that we change the "repack ($nr_packs)" test
to have the residual pack marked as kept (so we're measuring time it
takes to repack everything that _isn't_ in the base pack)?
That would allow a more direct comparison, but I think it's loosing out
on an important aspect which is how long it takes to pack the entire
repository. Maybe we want three.
That was what I was suggesting, but I think it's equivalent to what your
--stdin-packs is testing. I guess the most interesting thing would
actually be an _additional_ pack mark as .keep (and that pack does not
even have to contain anything interesting -- the point is how much
effort it costs to find that out. Of course the bigger it is the more
pronounced the effect of avoiding lookups in it).
-Peff
From: Jeff King <hidden> Date: 2021-02-17 19:30:06
On Wed, Feb 17, 2021 at 02:23:27PM -0500, Taylor Blau wrote:
On Wed, Feb 17, 2021 at 11:05:22AM -0500, Jeff King wrote:
quoted
I think just bumping that:
if (local && !p->pack_local)
return 0;
quoted
above the new code would fix it. Or to lay out the logic more fully, the
order of checks should be:
quoted
- does _this_ pack we found the object in disqualify it. If so, we can
cheaply return 0. And that applies to both keep and local rules.
- otherwise, check all packs via has_object_kept_pack(), which is
cheaper than continuing to iterate through all packs by returning
-1.
- once we know definitively about keep-packs, then check any shortcuts
related to local packs (like !have_non_local_packs)
- and then if no shortcuts, we return -1
I don't understand what you're suggesting. Is the (local &&
!p->pack_local) a disqualifying condition? Reading the comment, I think
it is, and so we could do something like:
That's exactly what I'm suggesting. If we have a non-local pack and were
given --local, then we can shortcut immediately without caring about
kept packs: we know that we do not want the object.
[...]
But your "check any shortcuts related to local packs" makes me think
that we should leave the code as-is.
No, the "shortcuts" there is the opposite:
if (!local || !have_non_local_packs)
return 1;
If either of those is true, we can say "definitely include" but only
with respect to the --local requirement. So we _can't_ bump that up, but
must check it only after we've definitively resolved the keep-pack
requirement.
-Peff
@@ -1225,9 +1225,9 @@ static int want_found_object(const struct object_id *oid, int exclude,*/unsignedflags=0;if(ignore_packed_keep_on_disk)-flags|=ON_DISK_KEEP_PACKS;+flags|=CACHE_ON_DISK_KEEP_PACKS;if(ignore_packed_keep_in_core)-flags|=IN_CORE_KEEP_PACKS;+flags|=CACHE_IN_CORE_KEEP_PACKS;
Why are we renaming the constants in this patch?
I know I'm listed as the author, but I think this came out of some
off-list back and forth between us. It seems like the existing constants
would have been fine.
Yeah, they would have been fine. They were renamed because this patch
makes them only used for the kept pack cache, but I agree the existing
names are fine, too.
In any case, they make an easier-to-read diff, so I'm perfectly happy to
un-rename them ;).
OK, so we keep a single cache based on the flags, and then if somebody
ever asks for different flags, we throw it away. That's probably OK for
our purposes, since we wouldn't expect multiple callers within a single
process.
I wondered if it would be simpler to just keep two lists, one for
in-core keeps and one for on-disk keeps. And then just walk over each
list separately based on the query flags. That makes things more robust
_and_ I think would be less code. It does mean that a pack could appear
in both lists, though, which means we might do a lookup in it twice.
That doesn't seem all that likely, but it is working against our goal
here.
Another option is to keep 3 caches (two separate and one combined),
rather than flipping between them. I'm not sure if that would be less
code or not (it gets rid of the "invalidate" function, but you do have
to pick the right cache depending on the query flags).
Yet another option is to keep a cache of any that are marked as _either_
in core or on-disk keeps, and then decide to look up the object based on
the query flags. Then you just pay the cost to iterate over the list and
check the flags (which really is all this cache is helping with in the
first place).
All interesting ideas. In this patch (and by the end of the series)
callers that use the kept pack cache never ask for the cache with a
different set of flags. IOW, there isn't a situation where a caller
would populate the in-core kept pack cache, and then suddenly ask for
both in-core and on-disk packs to be kept.
So all of this code is defensive in case that were to change, and
suddenly we'd be returning subtly wrong results. I could imagine that
being kind of a nasty bug to track down, so detecting and invalidating
the cache would make it a non-issue.
I'll note it in the commit message, though, since it's good for future
readers to be aware, too.
I dunno. TBH, I kind of wonder if this whole patch is worth doing at
all, giving the underwhelming performance benefit (3% on the
pathological 1000-pack case). When I had timed this strategy initially,
it was more like 15%. I'm not sure where the savings went in the
interim, or if it was a timing fluke.
Yeah, I dunno. It's certainly not hurting (I don't think the extra code
is all that complex, and the savings is at least non-zero), so I'm
inclined to keep it.
quoted
+static struct packed_git **kept_pack_cache(struct repository *r, unsigned flags)
+{
+ maybe_invalidate_kept_pack_cache(r, flags);
+
+ if (!r->objects->kept_pack_cache) {
+ struct packed_git **packs = NULL;
+ size_t nr = 0, alloc = 0;
+ struct packed_git *p;
+
+ /*
+ * We want "all" packs here, because we need to cover ones that
+ * are used by a midx, as well. We need to look in every one of
+ * them (instead of the midx itself) to cover duplicates. It's
+ * possible that an object is found in two packs that the midx
+ * covers, one kept and one not kept, but the midx returns only
+ * the non-kept version.
+ */
+ for (p = get_all_packs(r); p; p = p->next) {
+ if ((p->pack_keep && (flags & CACHE_ON_DISK_KEEP_PACKS)) ||
+ (p->pack_keep_in_core && (flags & CACHE_IN_CORE_KEEP_PACKS))) {
+ ALLOC_GROW(packs, nr + 1, alloc);
+ packs[nr++] = p;
+ }
+ }
+ ALLOC_GROW(packs, nr + 1, alloc);
+ packs[nr] = NULL;
+
+ r->objects->kept_pack_cache = xmalloc(sizeof(*r->objects->kept_pack_cache));
+ r->objects->kept_pack_cache->packs = packs;
+ r->objects->kept_pack_cache->flags = flags;
+ }
Is there any reason not to just embed the kept_pack_cache struct inside
the object_store? It's one less pointer to deal with. I wonder if this
is a holdover from an attempt to have multiple caches.
(I also think it would be reasonable if we wanted to hide the definition
of the cache struct from callers, but we don't seem do to that).
Not a holdover, just designed to avoid adding too many extra fields to
the object-store. I don't feel strongly, but I do think hiding the
definition is a good idea, so I'll inline it.
I notice that when the constants moved, we didn't keep an equivalent of
ALL_KEEP_PACKS. Maybe we didn't need it in the first place in patch 1?
Yeah, we didn't need it to begin with. I'll drop it accordingly.
BTW, I absolutely hate the complication that all of this on-disk
versus in-core keep distinction brings to this code. And I wondered
what it was really doing for us and whether we could get rid of it.
But I think we do need it: a common case may be to avoid using
--honor-pack-keep (because you don't want to deal with racy .keep
writes from incoming receive-pack processes), but use in-core ones for
something like --stdin-packs. So we do need to respect one and not the
other.
I do wonder if things would be simpler if pack-objects simply kept its
own list of "in core" packs in a separate array. But that is really
just another form of the same problem, I guess.
Yeah, the complexity is awfully hard to reason about, but you're right
that here it is necessary.
From: Taylor Blau <hidden> Date: 2021-02-17 20:02:09
On Wed, Feb 17, 2021 at 01:17:16PM -0500, Jeff King wrote:
On Wed, Feb 03, 2021 at 10:59:25PM -0500, Taylor Blau wrote:
quoted
Often it is useful to both:
- have relatively few packfiles in a repository, and
- avoid having so few packfiles in a repository that we repack its
entire contents regularly
This patch implements a '--geometric=<n>' option in 'git repack'. This
allows the caller to specify that they would like each pack to be at
least a factor times as large as the previous largest pack (by object
count).
Concretely, say that a repository has 'n' packfiles, labeled P1, P2,
..., up to Pn. Each packfile has an object count equal to 'objects(Pn)'.
With a geometric factor of 'r', it should be that:
objects(Pi) > r*objects(P(i-1))
for all i in [1, n], where the packs are sorted by
objects(P1) <= objects(P2) <= ... <= objects(Pn).
Just devil's advocating for a moment.
[large push becoming the biggest pack in a repository]
- it may have been more carefully packed (e.g., with a larger window
size, using "-f", etc) than the packs we got from pushes. We do
_mostly_ retain the deltas when we roll up the packs, so it probably
only has a small impact in practice (I'd expect in a few cases we'd
throw away deltas because a pushed pack contains a duplicate of its
base object that we added via --fix-thin).
Yeah, agreed.
So I suspect it's probably OK in practice. These cases would happen
rarely, and the impact would not be all that big. The bitmap thing I'd
worry the most about. As part of a larger strategy involving a midx it
is taken care of, but people using just this new feature may not realize
that. The bitmaps of course are "just" an optimization, but it's hard to
say how dire things are when they don't exist. For many situations,
probably not very dire. But I know that on our servers, when repos lack
bitmaps, people notice the performance degradation.
On the other hand, by definition this happens in a case where there are
more objects that have just been pushed (and are therefore not
bitmapped) than existed already. So you _already_ have a performance
problem either way until you get bitmap coverage of those new objects.
I almost split my reply between this and the above paragraph to say
exactly this. I think in this case you'd want to rewrite your bitmap
from scratch either way (whether you were using multi-pack or
traditional reachability bitmaps).
@@ -165,6 +165,17 @@ depth is 4095. Pass the `--delta-islands` option to `git-pack-objects`, see linkgit:git-pack-objects[1].+-g=<factor>::+--geometric=<factor>::+ Arrange resulting pack structure so that each successive pack+ contains at least `<factor>` times the number of objects as the+ next-largest pack.+++`git repack` ensures this by determining a "cut" of packfiles that need to be+repacked into one in order to ensure a geometric progression. It picks the+smallest set of packfiles such that as many of the larger packfiles (by count of+objects contained in that pack) may be left intact.
I think we might need to make clear in the documentation how this
differs from other repacks, in that it is not considering reachability
at all. I like the term "roll up" to describe what is happening, but we
probably need to define that term clearly, as well.
From: Jeff King <hidden> Date: 2021-02-17 20:26:29
On Wed, Feb 17, 2021 at 02:54:33PM -0500, Taylor Blau wrote:
quoted
OK, so we keep a single cache based on the flags, and then if somebody
ever asks for different flags, we throw it away. That's probably OK for
our purposes, since we wouldn't expect multiple callers within a single
process.
[...some alternatives]
All interesting ideas. In this patch (and by the end of the series)
callers that use the kept pack cache never ask for the cache with a
different set of flags. IOW, there isn't a situation where a caller
would populate the in-core kept pack cache, and then suddenly ask for
both in-core and on-disk packs to be kept.
So all of this code is defensive in case that were to change, and
suddenly we'd be returning subtly wrong results. I could imagine that
being kind of a nasty bug to track down, so detecting and invalidating
the cache would make it a non-issue.
Yeah, I agree that the current crop of callers does not care. And I am
glad we are not leaving a booby-trap for later programmers with respect
to correctness (by virtue of the invalidation function). But it does
feel like we are leaving one for performance, which they very well might
not realize the cache is doing worse-than-nothing.
Would just doing:
if (cache.packs && cache.flags != flags)
BUG("kept-pack-cache cannot handle multiple queries in a single process");
be a better solution? That is not helping anyone towards a world where
we gracefully handle back-and-forth queries. But it makes it abundantly
clear when such a thing would become necessary.
quoted
Is there any reason not to just embed the kept_pack_cache struct inside
the object_store? It's one less pointer to deal with. I wonder if this
is a holdover from an attempt to have multiple caches.
(I also think it would be reasonable if we wanted to hide the definition
of the cache struct from callers, but we don't seem do to that).
Not a holdover, just designed to avoid adding too many extra fields to
the object-store. I don't feel strongly, but I do think hiding the
definition is a good idea, so I'll inline it.
This response confuses me a bit. Hiding the definition from callers
would mean _keeping_ it as a pointer, but putting the definition into
packfile.c, where nobody outside that file could see it (at least that
is what I meant by hiding).
But inlining it to me implies embedding the struct (not a pointer to it)
in "struct object_store", defining the struct at the point we define the
struct field which uses it.
I am fine with either, to be clear. I'm just confused which you are
proposing to do. :)
-Peff
From: Taylor Blau <hidden> Date: 2021-02-17 20:33:13
On Wed, Feb 17, 2021 at 03:25:17PM -0500, Jeff King wrote:
Would just doing:
if (cache.packs && cache.flags != flags)
BUG("kept-pack-cache cannot handle multiple queries in a single process");
be a better solution? That is not helping anyone towards a world where
we gracefully handle back-and-forth queries. But it makes it abundantly
clear when such a thing would become necessary.
I dunno. I can certainly see its merits, but I have to imagine that
anybody who cares enough about the performance will be able to find our
conversation here. Assuming that's the case, I would rather have the
kept-pack cache handle multiple queries before BUG()-ing.
quoted
quoted
Is there any reason not to just embed the kept_pack_cache struct inside
the object_store? It's one less pointer to deal with. I wonder if this
is a holdover from an attempt to have multiple caches.
(I also think it would be reasonable if we wanted to hide the definition
of the cache struct from callers, but we don't seem do to that).
Not a holdover, just designed to avoid adding too many extra fields to
the object-store. I don't feel strongly, but I do think hiding the
definition is a good idea, so I'll inline it.
This response confuses me a bit. Hiding the definition from callers
would mean _keeping_ it as a pointer, but putting the definition into
packfile.c, where nobody outside that file could see it (at least that
is what I meant by hiding).
But inlining it to me implies embedding the struct (not a pointer to it)
in "struct object_store", defining the struct at the point we define the
struct field which uses it.
I am fine with either, to be clear. I'm just confused which you are
proposing to do. :)
Probably because I changed my mind in the middle of writing it ;). I'm
proposing embedding the definition of the struct into the definition of
object_store, and then operating on its fields (from within packfile.c).
Thanks,
Taylor
From: Jeff King <hidden> Date: 2021-02-17 21:44:30
On Wed, Feb 17, 2021 at 03:29:50PM -0500, Taylor Blau wrote:
On Wed, Feb 17, 2021 at 03:25:17PM -0500, Jeff King wrote:
quoted
Would just doing:
if (cache.packs && cache.flags != flags)
BUG("kept-pack-cache cannot handle multiple queries in a single process");
be a better solution? That is not helping anyone towards a world where
we gracefully handle back-and-forth queries. But it makes it abundantly
clear when such a thing would become necessary.
I dunno. I can certainly see its merits, but I have to imagine that
anybody who cares enough about the performance will be able to find our
conversation here. Assuming that's the case, I would rather have the
kept-pack cache handle multiple queries before BUG()-ing.
OK. I am on the fence, and you are the author, so I'm happy to go with
your preference.
I'm not quite as optimistic that somebody would find this conversation,
if only because they have to know to look for it. I could easily see
somebody adding a find_kept_in_pack() without thinking too hard about
it. OTOH, I find it quite unlikely that anybody would use a different
set of flags within the same process, so it would probably Just Work for
them regardless. :)
quoted
This response confuses me a bit. Hiding the definition from callers
would mean _keeping_ it as a pointer, but putting the definition into
packfile.c, where nobody outside that file could see it (at least that
is what I meant by hiding).
But inlining it to me implies embedding the struct (not a pointer to it)
in "struct object_store", defining the struct at the point we define the
struct field which uses it.
I am fine with either, to be clear. I'm just confused which you are
proposing to do. :)
Probably because I changed my mind in the middle of writing it ;). I'm
proposing embedding the definition of the struct into the definition of
object_store, and then operating on its fields (from within packfile.c).
OK, that sounds great to me (and arguably produces more efficient code,
since we avoid a pointer dereference, though I doubt it matters in
practice). Thanks for clarifying.
-Peff
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:17
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s).
In particular, an new 'git repack' mode which ensures the resulting
packs form a geometric progress by object count will mark packs that it
does not want to repack as "kept in-core", and it will want to halt a
reachability traversal as soon as it visits an object in any of the kept
packs. But, it does not want to halt the traversal at non-kept, or
.keep packs.
The obvious alternative is 'find_pack_entry()', but this doesn't quite
suffice since it only returns the first pack it finds, which may or may
not be kept (and the mru cache makes it unpredictable which one you'll
get if there are options).
Short of that, you could walk over all packs looking for the object in
each one, but it scales with the number of packs, which may be
prohibitive.
Introduce 'find_kept_pack_entry()', a function which is like
'find_pack_entry()', but only fills in objects in the kept packs.
Handle packs which have .keep files, as well as in-core kept packs
separately, since certain callers will want to distinguish one from the
other. (Though on-disk and in-core kept packs share the adjective
"kept", it is best to think of the two sets as independent.)
There is a gotcha when looking up objects that are duplicated in kept
and non-kept packs, particularly when the MIDX stores the non-kept
version and the caller asked for kept objects only. This could be
resolved by teaching the MIDX to resolve duplicates by always favoring
the kept pack (if one exists), but this breaks an assumption in existing
MIDXs, and so it would require a format change.
The benefit to changing the MIDX in this way is marginal, so we instead
have a more thorough check here which is explained with a comment.
Callers will be added in subsequent patches.
Co-authored-by: Jeff King [off-list ref]
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
packfile.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-----
packfile.h | 5 +++++
2 files changed, 64 insertions(+), 5 deletions(-)
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:17
Here is another updated version of mine and Peff's series to add a new 'git
repack --geometric' mode which supports repacking a repository into a geometric
progression of packs by object count.
(A previous version of this series depended on 'jk/p5303-sed-portability-fix',
but that topic has since been merged to 'master'. This series has been updated
to apply based on 'master' accordingly).
The series has not changed substantially since v2, but a range-diff is included
below for convenience. The most notable change is the new tests in p5303 were
reworked to provide a more equivalent comparison.
Beyond that, some minor code clean-up (embedding the kept-pack cache, making the
'--no-kept-packs' option of 'rev-list' undocumented, etc) has been applied to
address Peff's review.
Thanks in advance for another look at this series. I'm hopeful that this version
is in a good state to be queued so that it can make the 2.31 release, and users
can start playing with it.
Jeff King (4):
p5303: add missing &&-chains
p5303: measure time to repack with keep
builtin/pack-objects.c: rewrite honor-pack-keep logic
packfile: add kept-pack cache for find_kept_pack_entry()
Taylor Blau (4):
packfile: introduce 'find_kept_pack_entry()'
revision: learn '--no-kept-objects'
builtin/pack-objects.c: add '--stdin-packs' option
builtin/repack.c: add '--geometric' option
Documentation/git-pack-objects.txt | 10 +
Documentation/git-repack.txt | 22 ++
builtin/pack-objects.c | 329 ++++++++++++++++++++++++-----
builtin/repack.c | 187 +++++++++++++++-
object-store.h | 5 +
packfile.c | 67 ++++++
packfile.h | 5 +
revision.c | 15 ++
revision.h | 4 +
t/perf/p5303-many-packs.sh | 36 +++-
t/t5300-pack-object.sh | 97 +++++++++
t/t6114-keep-packs.sh | 69 ++++++
t/t7703-repack-geometric.sh | 137 ++++++++++++
13 files changed, 921 insertions(+), 62 deletions(-)
create mode 100755 t/t6114-keep-packs.sh
create mode 100755 t/t7703-repack-geometric.sh
Range-diff against v2:
[rebased onto 'master']
13: f7186147eb ! 1: aa94edf39b packfile: introduce 'find_kept_pack_entry()'
@@ packfile.h: int packed_object_info(struct repository *r,
+#define ON_DISK_KEEP_PACKS 1
+#define IN_CORE_KEEP_PACKS 2
-+#define ALL_KEEP_PACKS (ON_DISK_KEEP_PACKS | IN_CORE_KEEP_PACKS)
+
/*
* Iff a pack file in the given repository contains the object named by sha1,
14: ddc2896caa ! 2: 82f6b45463 revision: learn '--no-kept-objects'
@@ Commit message
certain packs alone (for e.g., when doing a geometric repack that has
some "large" packs which are kept in-core that it wants to leave alone).
+ Note that this option is not guaranteed to produce exactly the set of
+ objects that aren't in kept packs, since it's possible the traversal
+ order may end up in a situation where a non-kept ancestor was "cut off"
+ by a kept object (at which point we would stop traversing). But, we
+ don't care about absolute correctness here, since this will eventually
+ be used as a purely additive guide in an upcoming new repack mode.
+
+ Explicitly avoid documenting this new flag, since it is only used
+ internally. In theory we could avoid even adding it rev-list, but being
+ able to spell this option out on the command-line makes some special
+ cases easier to test without promising to keep it behaving consistently
+ forever. Those tricky cases are exercised in t6114.
+
Signed-off-by: Taylor Blau [off-list ref]
- ## Documentation/rev-list-options.txt ##
-@@ Documentation/rev-list-options.txt: ifdef::git-rev-list[]
- Only useful with `--objects`; print the object IDs that are not
- in packs.
-
-+--no-kept-objects[=<kind>]::
-+ Halts the traversal as soon as an object in a kept pack is
-+ found. If `<kind>` is `on-disk`, only packs with a corresponding
-+ `*.keep` file are ignored. If `<kind>` is `in-core`, only packs
-+ with their in-core kept state set are ignored. Otherwise, both
-+ kinds of kept packs are ignored.
-+
- --object-names::
- Only useful with `--objects`; print the names of the object IDs
- that are found. This is the default behavior.
-
- ## list-objects.c ##
-@@ list-objects.c: static void traverse_trees_and_blobs(struct traversal_context *ctx,
- ctx->show_object(obj, name, ctx->show_data);
- continue;
- }
-+ if (ctx->revs->no_kept_objects) {
-+ struct pack_entry e;
-+ if (find_kept_pack_entry(ctx->revs->repo, &obj->oid,
-+ ctx->revs->keep_pack_cache_flags,
-+ &e))
-+ continue;
-+ }
- if (!path)
- path = "";
- if (obj->type == OBJ_TREE) {
-
## revision.c ##
@@ revision.c: static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
revs->unpacked = 1;
15: c96b1bf995 ! 3: 033e4e3f67 builtin/pack-objects.c: add '--stdin-packs' option
@@ Documentation/git-pack-objects.txt: base-name::
can be useful to send new tags to native Git clients.
+--stdin-packs::
-+ Read the basenames of packfiles from the standard input, instead
-+ of object names or revision arguments. The resulting pack
-+ contains all objects listed in the included packs (those not
-+ beginning with `^`), excluding any objects listed in the
-+ excluded packs (beginning with `^`).
++ Read the basenames of packfiles (e.g., `pack-1234abcd.pack`)
++ from the standard input, instead of object names or revision
++ arguments. The resulting pack contains all objects listed in the
++ included packs (those not beginning with `^`), excluding any
++ objects listed in the excluded packs (beginning with `^`).
++
+Incompatible with `--revs`, or options that imply `--revs` (such as
+`--all`), with the exception of `--unpacked`, which is compatible.
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
return git_default_config(k, v, cb);
}
++/* Counters for trace2 output when in --stdin-packs mode. */
+static int stdin_packs_found_nr;
+static int stdin_packs_hints_nr;
+
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
+
+ display_progress(progress_state, ++nr_seen);
+
++ if (have_duplicate_entry(oid, 0))
++ return 0;
++
+ ofs = nth_packed_object_offset(p, pos);
++ if (!want_object_in_pack(oid, 0, &p, &ofs))
++ return 0;
+
+ oi.typep = &type;
+ if (packed_object_info(the_repository, p, ofs, &oi) < 0)
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
+ add_pending_oid(revs, NULL, oid, 0);
+ }
+
-+ if (have_duplicate_entry(oid, 0))
-+ return 0;
-+
-+ if (!want_object_in_pack(oid, 0, &p, &ofs))
-+ return 0;
-+
+ stdin_packs_found_nr++;
+
+ create_object_entry(oid, type, 0, 0, 0, p, ofs);
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
+
+static void show_commit_pack_hint(struct commit *commit, void *_data)
+{
++ /* nothing to do; commits don't have a namehash */
+}
+
+static void show_object_pack_hint(struct object *object, const char *name,
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
+ stdin_packs_hints_nr++;
+}
+
++static int pack_mtime_cmp(const void *_a, const void *_b)
++{
++ struct packed_git *a = ((const struct string_list_item*)_a)->util;
++ struct packed_git *b = ((const struct string_list_item*)_b)->util;
++
++ if (a->mtime < b->mtime)
++ return -1;
++ else if (b->mtime < a->mtime)
++ return 1;
++ else
++ return 0;
++}
++
+static void read_packs_list_from_stdin(void)
+{
+ struct strbuf buf = STRBUF_INIT;
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
+ die(_("could not find pack '%s'"), item->string);
+ p->pack_keep_in_core = 1;
+ }
++
++ /*
++ * Order packs by ascending mtime; use QSORT directly to access the
++ * string_list_item's ->util pointer, which string_list_sort() does not
++ * provide.
++ */
++ QSORT(include_packs.items, include_packs.nr, pack_mtime_cmp);
++
+ for_each_string_list_item(item, &include_packs) {
+ struct packed_git *p = item->util;
+ if (!p)
16: a46b7002b4 = 4: f9a5faf773 p5303: add missing &&-chains
17: b5081c01b5 ! 5: 181c104a03 p5303: measure time to repack with keep
@@ Metadata
## Commit message ##
p5303: measure time to repack with keep
- This is the same as the regular repack test, except that we mark the
- single base pack as "kept" and use --assume-kept-packs-closed. The
- theory is that this should be faster than the normal repack, because
- we'll have fewer objects to traverse and process.
+ Add two new tests to measure repack performance. Both test split the
+ repository into synthetic "pushes", and then leave the remaining objects
+ in a big base pack.
- Here are some timings on a recent clone of the kernel. In the
- single-pack case, there is nothing do since there are no non-excluded
- packs:
+ The first new test marks an empty pack as "kept" and then passes
+ --honor-pack-keep to avoid including objects in it. That doesn't change
+ the resulting pack, but it does let us compare to the normal repack case
+ to see how much overhead we add to check whether objects are kept or
+ not.
- 5303.5: repack (1) 57.42(54.88+10.64)
- 5303.6: repack with --stdin-packs (1) 0.01(0.01+0.00)
+ The other test is of --stdin-packs, which gives us a sense of how that
+ number scales based on the number of packs we provide as input. In each
+ of those tests, the empty pack isn't considered, but the residual pack
+ (objects that were left over and not included in one of the synthetic
+ push packs) is marked as kept.
- and in the 50-pack case, it is much faster to use `--stdin-packs`, since
- we avoid having to consider any objects in the excluded pack:
+ (Note that in the single-pack case of the --stdin-packs test, there is
+ nothing do since there are no non-excluded packs).
- 5303.10: repack (50) 71.26(88.24+4.96)
- 5303.11: repack with --stdin-packs (50) 3.49(11.82+0.28)
+ Here are some timings on a recent clone of the kernel:
- but our improvements vanish as we approach 1000 packs.
+ 5303.5: repack (1) 57.26(54.59+10.84)
+ 5303.6: repack with kept (1) 57.33(54.80+10.51)
- 5303.15: repack (1000) 215.64(491.33+14.80)
- 5303.16: repack with --stdin-packs (1000) 198.79(380.51+7.97)
+ in the 50-pack case, things start to slow down:
+
+ 5303.11: repack (50) 71.54(88.57+4.84)
+ 5303.12: repack with kept (50) 85.12(102.05+4.94)
+
+ and by the time we hit 1,000 packs, things are substantially worse, even
+ though the resulting pack produced is the same:
+
+ 5303.17: repack (1000) 216.87(490.79+14.57)
+ 5303.18: repack with kept (1000) 665.63(938.87+15.76)
+
+ Likewise, the scaling is pretty extreme on --stdin-packs:
+
+ 5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00)
+ 5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24)
+ 5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
@@ t/perf/p5303-many-packs.sh: repack_into_n () {
+ git pack-objects --delta-base-offset --revs staging/pack
+ ) &&
+ test_export base_pack &&
++
++ # create an empty packfile
++ empty_pack=$(git pack-objects staging/pack </dev/null) &&
++ test_export empty_pack &&
# and then incrementals between each pair of commits
last= &&
@@ t/perf/p5303-many-packs.sh: do
--stdout </dev/null >/dev/null
'
+
++ test_perf "repack with kept ($nr_packs)" '
++ git pack-objects --keep-true-parents \
++ --keep-pack=pack-$empty_pack.pack \
++ --honor-pack-keep --non-empty --all \
++ --reflog --indexed-objects --delta-base-offset \
++ --stdout </dev/null >/dev/null
++ '
++
+ test_perf "repack with --stdin-packs ($nr_packs)" '
+ git pack-objects \
+ --keep-true-parents \
18: c3868c7df9 ! 6: 67af143fd1 builtin/pack-objects.c: rewrite honor-pack-keep logic
@@ Commit message
packs are actually kept.
Note that we have to re-order the logic a bit here; we can deal with the
- "kept" situation completely, and then just fall back to the "--local"
- question. It might be worth having a similar optimized function to look
- at only local packs.
+ disqualifying situations first (e.g., finding the object in a non-local
+ pack with --local), then "kept" situation(s), and then just fall back to
+ other "--local" conditions.
Here are the results from p5303 (measurements again taken on the
kernel):
- Test HEAD^ HEAD
+ Test HEAD^ HEAD
-----------------------------------------------------------------------------------------------
- 5303.5: repack (1) 57.42(54.88+10.64) 57.44(54.71+10.78) +0.0%
- 5303.6: repack with --stdin-packs (1) 0.01(0.01+0.00) 0.01(0.00+0.01) +0.0%
- 5303.10: repack (50) 71.26(88.24+4.96) 71.32(88.38+4.90) +0.1%
- 5303.11: repack with --stdin-packs (50) 3.49(11.82+0.28) 3.43(11.81+0.22) -1.7%
- 5303.15: repack (1000) 215.64(491.33+14.80) 215.59(493.75+14.62) -0.0%
- 5303.16: repack with --stdin-packs (1000) 198.79(380.51+7.97) 131.44(314.24+8.11) -33.9%
-
- So our --stdin-packs case with many packs is now finally faster than the
- non-keep case (because it gets the speed benefit of looking at fewer
- objects, but not as big a penalty for looking at many packs).
+ 5303.5: repack (1) 57.26(54.59+10.84) 57.34(54.66+10.88) +0.1%
+ 5303.6: repack with kept (1) 57.33(54.80+10.51) 57.38(54.83+10.49) +0.1%
+ 5303.11: repack (50) 71.54(88.57+4.84) 71.70(88.99+4.74) +0.2%
+ 5303.12: repack with kept (50) 85.12(102.05+4.94) 72.58(89.61+4.78) -14.7%
+ 5303.17: repack (1000) 216.87(490.79+14.57) 217.19(491.72+14.25) +0.1%
+ 5303.18: repack with kept (1000) 665.63(938.87+15.76) 246.12(520.07+14.93) -63.0%
+
+ and the --stdin-packs timings:
+
+ 5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00) 0.00(0.00+0.00) -100.0%
+ 5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24) 3.43(11.75+0.24) -2.8%
+ 5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10) 130.50(307.15+7.66) -33.4%
+
+ So our repack with an empty .keep pack is roughly as fast as one without
+ a .keep pack up to 50 packs. But the --stdin-packs case scales a little
+ better, too.
+
+ Notably, it is faster than a repack of the same size and a kept pack. It
+ looks at fewer objects, of course, but the penalty for looking at many
+ packs isn't as costly.
Signed-off-by: Jeff King [off-list ref]
Signed-off-by: Taylor Blau [off-list ref]
@@ builtin/pack-objects.c: static int have_duplicate_entry(const struct object_id *
if (exclude)
return 1;
@@ builtin/pack-objects.c: static int want_found_object(int exclude, struct packed_git *p)
+ * make sure no copy of this object appears in _any_ pack that makes us
+ * to omit the object, so we need to check all the packs.
+ *
+- * We can however first check whether these options can possible matter;
++ * We can however first check whether these options can possibly matter;
+ * if they do not matter we know we want the object in generated pack.
* Otherwise, we signal "-1" at the end to tell the caller that we do
* not know either way, and it needs to check more packs.
*/
- if (!ignore_packed_keep_on_disk &&
- !ignore_packed_keep_in_core &&
- (!local || !have_non_local_packs))
+- return 1;
+
++ /*
++ * Objects in packs borrowed from elsewhere are discarded regardless of
++ * if they appear in other packs that weren't borrowed.
++ */
+ if (local && !p->pack_local)
+ return 0;
+- if (p->pack_local &&
+- ((ignore_packed_keep_on_disk && p->pack_keep) ||
+- (ignore_packed_keep_in_core && p->pack_keep_in_core)))
+- return 0;
+
+ /*
-+ * Handle .keep first, as we have a fast(er) path there.
++ * Then handle .keep first, as we have a fast(er) path there.
+ */
+ if (ignore_packed_keep_on_disk || ignore_packed_keep_in_core) {
+ /*
@@ builtin/pack-objects.c: static int want_found_object(int exclude, struct packed_
+ * keep-packs, or the object is not in one. Keep checking other
+ * conditions...
+ */
-+
+ if (!local || !have_non_local_packs)
- return 1;
--
- if (local && !p->pack_local)
- return 0;
-- if (p->pack_local &&
-- ((ignore_packed_keep_on_disk && p->pack_keep) ||
-- (ignore_packed_keep_in_core && p->pack_keep_in_core)))
-- return 0;
++ return 1;
/* we don't know yet; keep looking for more packs */
return -1;
19: f1c07324f6 ! 7: e9e04b95e7 packfile: add kept-pack cache for find_kept_pack_entry()
@@ Commit message
- we don't have to worry about any packed_git being removed; we always
keep the old structs around, even after reprepare_packed_git()
+ We do defensively invalidate the cache in case the set of kept packs
+ being asked for changes (e.g., only in-core kept packs were cached, but
+ suddenly the caller also wants on-disk kept packs, too). In theory we
+ could build all three caches and switch between them, but it's not
+ necessary, since this patch (and series) never changes the set of kept
+ packs that it wants to inspect from the cache.
+
+ So that "optimization" is more about being defensive in the face of
+ future changes than it is about asking for multiple kinds of kept packs
+ in this patch.
+
Here are p5303 results (as always, measured against the kernel):
Test HEAD^ HEAD
- ----------------------------------------------------------------------------------------------
- 5303.5: repack (1) 57.44(54.71+10.78) 57.06(54.29+10.96) -0.7%
- 5303.6: repack with --stdin-packs (1) 0.01(0.00+0.01) 0.01(0.01+0.00) +0.0%
- 5303.10: repack (50) 71.32(88.38+4.90) 71.47(88.60+5.04) +0.2%
- 5303.11: repack with --stdin-packs (50) 3.43(11.81+0.22) 3.49(12.21+0.26) +1.7%
- 5303.15: repack (1000) 215.59(493.75+14.62) 217.41(495.36+14.85) +0.8%
- 5303.16: repack with --stdin-packs (1000) 131.44(314.24+8.11) 126.75(309.88+8.09) -3.6%
+ -----------------------------------------------------------------------------------------------
+ 5303.5: repack (1) 57.34(54.66+10.88) 56.98(54.36+10.98) -0.6%
+ 5303.6: repack with kept (1) 57.38(54.83+10.49) 57.17(54.97+10.26) -0.4%
+ 5303.11: repack (50) 71.70(88.99+4.74) 71.62(88.48+5.08) -0.1%
+ 5303.12: repack with kept (50) 72.58(89.61+4.78) 71.56(88.80+4.59) -1.4%
+ 5303.17: repack (1000) 217.19(491.72+14.25) 217.31(490.82+14.53) +0.1%
+ 5303.18: repack with kept (1000) 246.12(520.07+14.93) 217.08(490.37+15.10) -11.8%
+
+ and the --stdin-packs case, which scales a little bit better (although
+ not by that much even at 1,000 packs):
+
+ 5303.7: repack with --stdin-packs (1) 0.00(0.00+0.00) 0.00(0.00+0.00) =
+ 5303.13: repack with --stdin-packs (50) 3.43(11.75+0.24) 3.43(11.69+0.30) +0.0%
+ 5303.19: repack with --stdin-packs (1000) 130.50(307.15+7.66) 125.13(301.36+8.04) -4.1%
Signed-off-by: Jeff King [off-list ref]
Signed-off-by: Taylor Blau [off-list ref]
- ## builtin/pack-objects.c ##
-@@ builtin/pack-objects.c: static int want_found_object(const struct object_id *oid, int exclude,
- */
- unsigned flags = 0;
- if (ignore_packed_keep_on_disk)
-- flags |= ON_DISK_KEEP_PACKS;
-+ flags |= CACHE_ON_DISK_KEEP_PACKS;
- if (ignore_packed_keep_in_core)
-- flags |= IN_CORE_KEEP_PACKS;
-+ flags |= CACHE_IN_CORE_KEEP_PACKS;
-
- if (ignore_packed_keep_on_disk && p->pack_keep)
- return 0;
-@@ builtin/pack-objects.c: static void read_packs_list_from_stdin(void)
- * an optimization during delta selection.
- */
- revs.no_kept_objects = 1;
-- revs.keep_pack_cache_flags |= IN_CORE_KEEP_PACKS;
-+ revs.keep_pack_cache_flags |= CACHE_IN_CORE_KEEP_PACKS;
- revs.blob_objects = 1;
- revs.tree_objects = 1;
- revs.tag_objects = 1;
-
## object-store.h ##
-@@ object-store.h: static inline int pack_map_entry_cmp(const void *unused_cmp_data,
- return strcmp(pg1->pack_name, key ? key : pg2->pack_name);
- }
-
-+#define CACHE_ON_DISK_KEEP_PACKS 1
-+#define CACHE_IN_CORE_KEEP_PACKS 2
-+
-+struct kept_pack_cache {
-+ struct packed_git **packs;
-+ unsigned flags;
-+};
-+
- struct raw_object_store {
- /*
- * Set of all object directories; the main directory is first (and
@@ object-store.h: struct raw_object_store {
/* A most-recently-used ordered version of the packed_git list. */
struct list_head packed_git_mru;
-+ struct kept_pack_cache *kept_pack_cache;
++ struct {
++ struct packed_git **packs;
++ unsigned flags;
++ } kept_pack_cache;
+
/*
* A map of packfiles to packed_git structs for tracking which
@@ packfile.c: static int find_one_pack_entry(struct repository *r,
+ unsigned flags)
{
- return find_one_pack_entry(r, oid, e, 0);
-+ if (!r->objects->kept_pack_cache)
++ if (!r->objects->kept_pack_cache.packs)
+ return;
-+ if (r->objects->kept_pack_cache->flags == flags)
++ if (r->objects->kept_pack_cache.flags == flags)
+ return;
-+ free(r->objects->kept_pack_cache->packs);
-+ FREE_AND_NULL(r->objects->kept_pack_cache);
++ FREE_AND_NULL(r->objects->kept_pack_cache.packs);
++ r->objects->kept_pack_cache.flags = 0;
+}
+
+static struct packed_git **kept_pack_cache(struct repository *r, unsigned flags)
+{
+ maybe_invalidate_kept_pack_cache(r, flags);
+
-+ if (!r->objects->kept_pack_cache) {
++ if (!r->objects->kept_pack_cache.packs) {
+ struct packed_git **packs = NULL;
+ size_t nr = 0, alloc = 0;
+ struct packed_git *p;
@@ packfile.c: static int find_one_pack_entry(struct repository *r,
+ * the non-kept version.
+ */
+ for (p = get_all_packs(r); p; p = p->next) {
-+ if ((p->pack_keep && (flags & CACHE_ON_DISK_KEEP_PACKS)) ||
-+ (p->pack_keep_in_core && (flags & CACHE_IN_CORE_KEEP_PACKS))) {
++ if ((p->pack_keep && (flags & ON_DISK_KEEP_PACKS)) ||
++ (p->pack_keep_in_core && (flags & IN_CORE_KEEP_PACKS))) {
+ ALLOC_GROW(packs, nr + 1, alloc);
+ packs[nr++] = p;
+ }
@@ packfile.c: static int find_one_pack_entry(struct repository *r,
+ ALLOC_GROW(packs, nr + 1, alloc);
+ packs[nr] = NULL;
+
-+ r->objects->kept_pack_cache = xmalloc(sizeof(*r->objects->kept_pack_cache));
-+ r->objects->kept_pack_cache->packs = packs;
-+ r->objects->kept_pack_cache->flags = flags;
++ r->objects->kept_pack_cache.packs = packs;
++ r->objects->kept_pack_cache.flags = flags;
+ }
+
-+ return r->objects->kept_pack_cache->packs;
++ return r->objects->kept_pack_cache.packs;
}
int find_kept_pack_entry(struct repository *r,
@@ packfile.c: int find_kept_pack_entry(struct repository *r,
}
int has_object_pack(const struct object_id *oid)
-@@ packfile.c: int has_object_pack(const struct object_id *oid)
- return find_pack_entry(the_repository, oid, &e);
- }
-
--int has_object_kept_pack(const struct object_id *oid, unsigned flags)
-+int has_object_kept_pack(const struct object_id *oid,
-+ unsigned flags)
- {
- struct pack_entry e;
- return find_kept_pack_entry(the_repository, oid, flags, &e);
-
- ## packfile.h ##
-@@ packfile.h: int packed_object_info(struct repository *r,
- void mark_bad_packed_object(struct packed_git *p, const unsigned char *sha1);
- const struct packed_git *has_packed_and_bad(struct repository *r, const unsigned char *sha1);
-
--#define ON_DISK_KEEP_PACKS 1
--#define IN_CORE_KEEP_PACKS 2
--#define ALL_KEEP_PACKS (ON_DISK_KEEP_PACKS | IN_CORE_KEEP_PACKS)
--
- /*
- * Iff a pack file in the given repository contains the object named by sha1,
- * return true and store its location to e.
-
- ## revision.c ##
-@@ revision.c: static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
- die(_("--unpacked=<packfile> no longer supported"));
- } else if (!strcmp(arg, "--no-kept-objects")) {
- revs->no_kept_objects = 1;
-- revs->keep_pack_cache_flags |= IN_CORE_KEEP_PACKS;
-- revs->keep_pack_cache_flags |= ON_DISK_KEEP_PACKS;
-+ revs->keep_pack_cache_flags |= CACHE_IN_CORE_KEEP_PACKS;
-+ revs->keep_pack_cache_flags |= CACHE_ON_DISK_KEEP_PACKS;
- } else if (skip_prefix(arg, "--no-kept-objects=", &optarg)) {
- revs->no_kept_objects = 1;
- if (!strcmp(optarg, "in-core"))
-- revs->keep_pack_cache_flags |= IN_CORE_KEEP_PACKS;
-+ revs->keep_pack_cache_flags |= CACHE_IN_CORE_KEEP_PACKS;
- if (!strcmp(optarg, "on-disk"))
-- revs->keep_pack_cache_flags |= ON_DISK_KEEP_PACKS;
-+ revs->keep_pack_cache_flags |= CACHE_ON_DISK_KEEP_PACKS;
- } else if (!strcmp(arg, "-r")) {
- revs->diff = 1;
- revs->diffopt.flags.recursive = 1;
20: d5561585c2 ! 8: bd492ec142 builtin/repack.c: add '--geometric' option
@@ Documentation/git-repack.txt: depth is 4095.
+ contains at least `<factor>` times the number of objects as the
+ next-largest pack.
++
-+`git repack` ensures this by determining a "cut" of packfiles that need to be
-+repacked into one in order to ensure a geometric progression. It picks the
-+smallest set of packfiles such that as many of the larger packfiles (by count of
-+objects contained in that pack) may be left intact.
++`git repack` ensures this by determining a "cut" of packfiles that need
++to be repacked into one in order to ensure a geometric progression. It
++picks the smallest set of packfiles such that as many of the larger
++packfiles (by count of objects contained in that pack) may be left
++intact.
+++
++Unlike other repack modes, the set of objects to pack is determined
++uniquely by the set of packs being "rolled-up"; in other words, the
++packs determined to need to be combined in order to restore a geometric
++progression.
+++
++Loose objects are implicitly included in this "roll-up", without respect
++to their reachability. This is subject to change in the future. This
++option (implying a drastically different repack mode) is not guarenteed
++to work with all other combinations of option to `git repack`).
+
Configuration
-------------
--
2.30.0.667.g81c0cbc6fd
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:21
A future caller will want to be able to perform a reachability traversal
which terminates when visiting an object found in a kept pack. The
closest existing option is '--honor-pack-keep', but this isn't quite
what we want. Instead of halting the traversal midway through, a full
traversal is always performed, and the results are only trimmed
afterwords.
Besides needing to introduce a new flag (since culling results
post-facto can be different than halting the traversal as it's
happening), there is an additional wrinkle handling the distinction
in-core and on-disk kept packs. That is: what kinds of kept pack should
stop the traversal?
Introduce '--no-kept-objects[=<on-disk|in-core>]' to specify which kinds
of kept packs, if any, should stop a traversal. This can be useful for
callers that want to perform a reachability analysis, but want to leave
certain packs alone (for e.g., when doing a geometric repack that has
some "large" packs which are kept in-core that it wants to leave alone).
Note that this option is not guaranteed to produce exactly the set of
objects that aren't in kept packs, since it's possible the traversal
order may end up in a situation where a non-kept ancestor was "cut off"
by a kept object (at which point we would stop traversing). But, we
don't care about absolute correctness here, since this will eventually
be used as a purely additive guide in an upcoming new repack mode.
Explicitly avoid documenting this new flag, since it is only used
internally. In theory we could avoid even adding it rev-list, but being
able to spell this option out on the command-line makes some special
cases easier to test without promising to keep it behaving consistently
forever. Those tricky cases are exercised in t6114.
Signed-off-by: Taylor Blau <redacted>
---
revision.c | 15 ++++++++++
revision.h | 4 +++
t/t6114-keep-packs.sh | 69 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 88 insertions(+)
create mode 100755 t/t6114-keep-packs.sh
@@ -0,0 +1,69 @@+#!/bin/sh++test_description='rev-list with .keep packs'+../test-lib.sh++test_expect_success'setup''+test_commitloose&&+test_commitpacked&&+test_commitkept&&++KEPT_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/kept+^refs/tags/packed+EOF+)&&+MISC_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/packed+^refs/tags/loose+EOF+)&&++touch.git/objects/pack/pack-$KEPT_PACK.keep+'++rev_list_objects(){+gitrev-list"$@">out&&+sortout+}++idx_objects(){+gitshow-index<$1>expect-idx&&+cut-d" "-f2<expect-idx|sort+}++test_expect_success'--no-kept-objects excludes trees and blobs in .keep packs''+rev_list_objects--objects--all--no-object-names>kept&&+rev_list_objects--objects--all--no-object-names--no-kept-objects>no-kept&&++idx_objects.git/objects/pack/pack-$KEPT_PACK.idx>expect&&+comm-3keptno-kept>actual&&++test_cmpexpectactual+'++test_expect_success'--no-kept-objects excludes kept non-MIDX object''+test_configcore.multiPackIndextrue&&++# Create a pack with just the commit object in pack, and do not mark it+# as kept (even though it appears in $KEPT_PACK, which does have a .keep+# file).+MIDX_PACK=$(gitpack-objects.git/objects/pack/pack<<-EOF+$(gitrev-parsekept)+EOF+)&&++# Write a MIDX containing all packs, but use the version of the commit+# at "kept" in a non-kept pack by touching $MIDX_PACK.+touch.git/objects/pack/pack-$MIDX_PACK.pack&&+gitmulti-pack-indexwrite&&++rev_list_objects--objects--no-object-names--no-kept-objectsHEAD>actual&&+(+idx_objects.git/objects/pack/pack-$MISC_PACK.idx&&+gitrev-list--objects--no-object-namesrefs/tags/loose+)|sort>expect&&+test_cmpexpectactual+'++test_done
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:24
From: Jeff King <redacted>
These are in a helper function, so the usual chain-lint doesn't notice
them. This function is still not perfect, as it has some git invocations
on the left-hand-side of the pipe, but it's primary purpose is timing,
not finding bugs or correctness issues.
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -28,11 +28,11 @@ repack_into_n () {push@commits,$_if$.%5==1;}printreverse@commits;-'"$1">pushes+'"$1">pushes&&# create base packfilehead-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack+gitpack-objects--delta-base-offset--revsstaging/pack&&# and then incrementals between each pair of commitslast=&&
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:25
In an upcoming commit, 'git repack' will want to create a pack comprised
of all of the objects in some packs (the included packs) excluding any
objects in some other packs (the excluded packs).
This caller could iterate those packs themselves and feed the objects it
finds to 'git pack-objects' directly over stdin, but this approach has a
few downsides:
- It requires every caller that wants to drive 'git pack-objects' in
this way to implement pack iteration themselves. This forces the
caller to think about details like what order objects are fed to
pack-objects, which callers would likely rather not do.
- If the set of objects in included packs is large, it requires
sending a lot of data over a pipe, which is inefficient.
- The caller is forced to keep track of the excluded objects, too, and
make sure that it doesn't send any objects that appear in both
included and excluded packs.
But the biggest downside is the lack of a reachability traversal.
Because the caller passes in a list of objects directly, those objects
don't get a namehash assigned to them, which can have a negative impact
on the delta selection process, causing 'git pack-objects' to fail to
find good deltas even when they exist.
The caller could formulate a reachability traversal themselves, but the
only way to drive 'git pack-objects' in this way is to do a full
traversal, and then remove objects in the excluded packs after the
traversal is complete. This can be detrimental to callers who care
about performance, especially in repositories with many objects.
Introduce 'git pack-objects --stdin-packs' which remedies these four
concerns.
'git pack-objects --stdin-packs' expects a list of pack names on stdin,
where 'pack-xyz.pack' denotes that pack as included, and
'^pack-xyz.pack' denotes it as excluded. The resulting pack includes all
objects that are present in at least one included pack, and aren't
present in any excluded pack.
To address the delta selection problem, 'git pack-objects --stdin-packs'
works as follows. First, it assembles a list of objects that it is going
to pack, as above. Then, a reachability traversal is started, whose tips
are any commits mentioned in included packs. Upon visiting an object, we
find its corresponding object_entry in the to_pack list, and set its
namehash parameter appropriately.
To avoid the traversal visiting more objects than it needs to, the
traversal is halted upon encountering an object which can be found in an
excluded pack (by marking the excluded packs as kept in-core, and
passing --no-kept-objects=in-core to the revision machinery).
This can cause the traversal to halt early, for example if an object in
an included pack is an ancestor of ones in excluded packs. But stopping
early is OK, since filling in the namehash fields of objects in the
to_pack list is only additive (i.e., having it helps the delta selection
process, but leaving it blank doesn't impact the correctness of the
resulting pack).
Even still, it is unlikely that this hurts us much in practice, since
the 'git repack --geometric' caller (which is introduced in a later
commit) marks small packs as included, and large ones as excluded.
During ordinary use, the small packs usually represent pushes after a
large repack, and so are unlikely to be ancestors of objects that
already exist in the repository.
(I found it convenient while developing this patch to have 'git
pack-objects' report the number of objects which were visited and got
their namehash fields filled in during traversal. This is also included
in the below patch via trace2 data lines).
Suggested-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 10 ++
builtin/pack-objects.c | 198 ++++++++++++++++++++++++++++-
t/t5300-pack-object.sh | 97 ++++++++++++++
3 files changed, 303 insertions(+), 2 deletions(-)
@@ -85,6 +85,16 @@ base-name:: reference was included in the resulting packfile. This can be useful to send new tags to native Git clients.+--stdin-packs::+ Read the basenames of packfiles (e.g., `pack-1234abcd.pack`)+ from the standard input, instead of object names or revision+ arguments. The resulting pack contains all objects listed in the+ included packs (those not beginning with `^`), excluding any+ objects listed in the excluded packs (beginning with `^`).+++Incompatible with `--revs`, or options that imply `--revs` (such as+`--all`), with the exception of `--unpacked`, which is compatible.+ --window=<n>:: --depth=<n>:: These two options affect how the objects contained in
@@ -2986,6 +2986,186 @@ static int git_pack_config(const char *k, const char *v, void *cb)returngit_default_config(k,v,cb);}+/* Counters for trace2 output when in --stdin-packs mode. */+staticintstdin_packs_found_nr;+staticintstdin_packs_hints_nr;++staticintadd_object_entry_from_pack(conststructobject_id*oid,+structpacked_git*p,+uint32_tpos,+void*_data)+{+structrev_info*revs=_data;+structobject_infooi=OBJECT_INFO_INIT;+off_tofs;+enumobject_typetype;++display_progress(progress_state,++nr_seen);++if(have_duplicate_entry(oid,0))+return0;++ofs=nth_packed_object_offset(p,pos);+if(!want_object_in_pack(oid,0,&p,&ofs))+return0;++oi.typep=&type;+if(packed_object_info(the_repository,p,ofs,&oi)<0)+die(_("could not get type of object %s in pack %s"),+oid_to_hex(oid),p->pack_name);+elseif(type==OBJ_COMMIT){+/*+*commitsinincludedpacksareusedasstartingpointsforthe+*subsequentrevisionwalk+*/+add_pending_oid(revs,NULL,oid,0);+}++stdin_packs_found_nr++;++create_object_entry(oid,type,0,0,0,p,ofs);++return0;+}++staticvoidshow_commit_pack_hint(structcommit*commit,void*_data)+{+/* nothing to do; commits don't have a namehash */+}++staticvoidshow_object_pack_hint(structobject*object,constchar*name,+void*_data)+{+structobject_entry*oe=packlist_find(&to_pack,&object->oid);+if(!oe)+return;++/*+*Our'to_pack'listwasconstructedbyiteratingallobjectspackedin+*includedpacks,andsodoesn'thaveanon-zerohashfieldthatyou+*wouldtypicallypickupduringareachabilitytraversal.+*+*Makeabest-effortattempttofillinthe->hashand->no_try_delta+*hereusinganowinordertoperhapsimprovethedeltaselection+*process.+*/+oe->hash=pack_name_hash(name);+oe->no_try_delta=name&&no_try_delta(name);++stdin_packs_hints_nr++;+}++staticintpack_mtime_cmp(constvoid*_a,constvoid*_b)+{+structpacked_git*a=((conststructstring_list_item*)_a)->util;+structpacked_git*b=((conststructstring_list_item*)_b)->util;++if(a->mtime<b->mtime)+return-1;+elseif(b->mtime<a->mtime)+return1;+else+return0;+}++staticvoidread_packs_list_from_stdin(void)+{+structstrbufbuf=STRBUF_INIT;+structstring_listinclude_packs=STRING_LIST_INIT_DUP;+structstring_listexclude_packs=STRING_LIST_INIT_DUP;+structstring_list_item*item=NULL;++structpacked_git*p;+structrev_inforevs;++repo_init_revisions(the_repository,&revs,NULL);+/*+*Usearevisionwalktofillinthenamehashofobjectsintheinclude+*packs.Tosavetime,we'llavoidtraversingthroughobjectsthatare+*inexcludedpacks.+*+*Thatmaycauseustoavoidpopulatingallofthenamehashfieldsof+*allincludedobjects,butourgoalisbest-effort,sincethisisonly+*anoptimizationduringdeltaselection.+*/+revs.no_kept_objects=1;+revs.keep_pack_cache_flags|=IN_CORE_KEEP_PACKS;+revs.blob_objects=1;+revs.tree_objects=1;+revs.tag_objects=1;++while(strbuf_getline(&buf,stdin)!=EOF){+if(!buf.len)+continue;++if(*buf.buf=='^')+string_list_append(&exclude_packs,buf.buf+1);+else+string_list_append(&include_packs,buf.buf);++strbuf_reset(&buf);+}++string_list_sort(&include_packs);+string_list_sort(&exclude_packs);++for(p=get_all_packs(the_repository);p;p=p->next){+constchar*pack_name=pack_basename(p);++item=string_list_lookup(&include_packs,pack_name);+if(!item)+item=string_list_lookup(&exclude_packs,pack_name);++if(item)+item->util=p;+}++/*+*Firsthandlealloftheexcludedpacks,markingthemaskeptin-core+*sothatlatercallstoadd_object_entry()discardsanyobjectsthat+*arealsofoundinexcludedpacks.+*/+for_each_string_list_item(item,&exclude_packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+p->pack_keep_in_core=1;+}++/*+*Orderpacksbyascendingmtime;useQSORTdirectlytoaccessthe+*string_list_item's->utilpointer,whichstring_list_sort()doesnot+*provide.+*/+QSORT(include_packs.items,include_packs.nr,pack_mtime_cmp);++for_each_string_list_item(item,&include_packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+for_each_object_in_pack(p,+add_object_entry_from_pack,+&revs,+FOR_EACH_OBJECT_PACK_ORDER);+}++if(prepare_revision_walk(&revs))+die(_("revision walk setup failed"));+traverse_commit_list(&revs,+show_commit_pack_hint,+show_object_pack_hint,+NULL);++trace2_data_intmax("pack-objects",the_repository,"stdin_packs_found",+stdin_packs_found_nr);+trace2_data_intmax("pack-objects",the_repository,"stdin_packs_hints",+stdin_packs_hints_nr);++strbuf_release(&buf);+string_list_clear(&include_packs,0);+string_list_clear(&exclude_packs,0);+}+staticvoidread_object_list_from_stdin(void){charline[GIT_MAX_HEXSZ+1+PATH_MAX+2];
@@ -3539,6 +3720,8 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)OPT_SET_INT_F(0,"indexed-objects",&rev_list_index,N_("include objects referred to by the index"),1,PARSE_OPT_NONEG),+OPT_BOOL(0,"stdin-packs",&stdin_packs,+N_("read packs from stdin")),OPT_BOOL(0,"stdout",&pack_to_stdout,N_("output pack to stdout")),OPT_BOOL(0,"include-tag",&include_tag,
@@ -3690,8 +3873,13 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(filter_options.choice){if(!pack_to_stdout)die(_("cannot use --filter without --stdout"));+if(stdin_packs)+die(_("cannot use --filter with --stdin-packs"));}+if(stdin_packs&&use_internal_rev_list)+die(_("cannot use internal rev list with --stdin-packs"));+/**"soft"reasonsnottousebitmaps-foron-diskrepackbydefaultwewant*
@@ -532,4 +532,101 @@ test_expect_success 'prefetch objects' 'test_line_count=1donelines'+test_expect_success'setup for --stdin-packs tests''+gitinitstdin-packs&&+(+cdstdin-packs&&++test_commitA&&+test_commitB&&+test_commitC&&++foridinABC+do+gitpack-objects.git/objects/pack/pack-$id\+--incremental--revs<<-EOF+refs/tags/$id+EOF+done&&++ls-la.git/objects/pack+)+'++test_expect_success'--stdin-packs with excluded packs''+(+cdstdin-packs&&++PACK_A="$(basename.git/objects/pack/pack-A-*.pack)"&&+PACK_B="$(basename.git/objects/pack/pack-B-*.pack)"&&+PACK_C="$(basename.git/objects/pack/pack-C-*.pack)"&&++gitpack-objectstest--stdin-packs<<-EOF&&+$PACK_A+^$PACK_B+$PACK_C+EOF++(+gitshow-index<$(ls.git/objects/pack/pack-A-*.idx)&&+gitshow-index<$(ls.git/objects/pack/pack-C-*.idx)+)>expect.raw&&+gitshow-index<$(lstest-*.idx)>actual.raw&&++cut-d" "-f2<expect.raw|sort>expect&&+cut-d" "-f2<actual.raw|sort>actual&&+test_cmpexpectactual+)+'++test_expect_success'--stdin-packs is incompatible with --filter''+(+cdstdin-packs&&+test_must_failgitpack-objects--stdin-packs--stdout\+--filter=blob:none</dev/null2>err&&+test_i18ngrep"cannot use --filter with --stdin-packs"err+)+'++test_expect_success'--stdin-packs is incompatible with --revs''+(+cdstdin-packs&&+test_must_failgitpack-objects--stdin-packs--revsout\+</dev/null2>err&&+test_i18ngrep"cannot use internal rev list with --stdin-packs"err+)+'++test_expect_success'--stdin-packs with loose objects''+(+cdstdin-packs&&++PACK_A="$(basename.git/objects/pack/pack-A-*.pack)"&&+PACK_B="$(basename.git/objects/pack/pack-B-*.pack)"&&+PACK_C="$(basename.git/objects/pack/pack-C-*.pack)"&&++test_commitD&&# loose++gitpack-objectstest2--stdin-packs--unpacked<<-EOF&&+$PACK_A+^$PACK_B+$PACK_C+EOF++(+gitshow-index<$(ls.git/objects/pack/pack-A-*.idx)&&+gitshow-index<$(ls.git/objects/pack/pack-C-*.idx)&&+gitrev-list--objects--no-object-names\+refs/tags/C..refs/tags/D++)>expect.raw&&+ls-la.&&+gitshow-index<$(lstest2-*.idx)>actual.raw&&++cut-d" "-f2<expect.raw|sort>expect&&+cut-d" "-f2<actual.raw|sort>actual&&+test_cmpexpectactual+)+'+ test_done
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:41
From: Jeff King <redacted>
Now that we have find_kept_pack_entry(), we don't have to manually keep
hunting through every pack to find a possible "kept" duplicate of the
object. This should be faster, assuming only a portion of your total
packs are actually kept.
Note that we have to re-order the logic a bit here; we can deal with the
disqualifying situations first (e.g., finding the object in a non-local
pack with --local), then "kept" situation(s), and then just fall back to
other "--local" conditions.
Here are the results from p5303 (measurements again taken on the
kernel):
Test HEAD^ HEAD
-----------------------------------------------------------------------------------------------
5303.5: repack (1) 57.26(54.59+10.84) 57.34(54.66+10.88) +0.1%
5303.6: repack with kept (1) 57.33(54.80+10.51) 57.38(54.83+10.49) +0.1%
5303.11: repack (50) 71.54(88.57+4.84) 71.70(88.99+4.74) +0.2%
5303.12: repack with kept (50) 85.12(102.05+4.94) 72.58(89.61+4.78) -14.7%
5303.17: repack (1000) 216.87(490.79+14.57) 217.19(491.72+14.25) +0.1%
5303.18: repack with kept (1000) 665.63(938.87+15.76) 246.12(520.07+14.93) -63.0%
and the --stdin-packs timings:
5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00) 0.00(0.00+0.00) -100.0%
5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24) 3.43(11.75+0.24) -2.8%
5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10) 130.50(307.15+7.66) -33.4%
So our repack with an empty .keep pack is roughly as fast as one without
a .keep pack up to 50 packs. But the --stdin-packs case scales a little
better, too.
Notably, it is faster than a repack of the same size and a kept pack. It
looks at fewer objects, of course, but the penalty for looking at many
packs isn't as costly.
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 131 ++++++++++++++++++++++++-----------------
1 file changed, 78 insertions(+), 53 deletions(-)
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:42
From: Jeff King <redacted>
Add two new tests to measure repack performance. Both test split the
repository into synthetic "pushes", and then leave the remaining objects
in a big base pack.
The first new test marks an empty pack as "kept" and then passes
--honor-pack-keep to avoid including objects in it. That doesn't change
the resulting pack, but it does let us compare to the normal repack case
to see how much overhead we add to check whether objects are kept or
not.
The other test is of --stdin-packs, which gives us a sense of how that
number scales based on the number of packs we provide as input. In each
of those tests, the empty pack isn't considered, but the residual pack
(objects that were left over and not included in one of the synthetic
push packs) is marked as kept.
(Note that in the single-pack case of the --stdin-packs test, there is
nothing do since there are no non-excluded packs).
Here are some timings on a recent clone of the kernel:
5303.5: repack (1) 57.26(54.59+10.84)
5303.6: repack with kept (1) 57.33(54.80+10.51)
in the 50-pack case, things start to slow down:
5303.11: repack (50) 71.54(88.57+4.84)
5303.12: repack with kept (50) 85.12(102.05+4.94)
and by the time we hit 1,000 packs, things are substantially worse, even
though the resulting pack produced is the same:
5303.17: repack (1000) 216.87(490.79+14.57)
5303.18: repack with kept (1000) 665.63(938.87+15.76)
Likewise, the scaling is pretty extreme on --stdin-packs:
5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00)
5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24)
5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Our solution to that was to notice that most repos don't have keep
files, and to make that case a fast path. But as soon as you add a
single .keep, that part of pack-objects slows down again (even if we
have fewer objects total to look at).
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 34 ++++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
@@ -31,8 +31,15 @@ repack_into_n () {'"$1">pushes&&# create base packfile-head-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack&&+base_pack=$(+head-n1pushes|+gitpack-objects--delta-base-offset--revsstaging/pack+)&&+test_exportbase_pack&&++# create an empty packfile+empty_pack=$(gitpack-objectsstaging/pack</dev/null)&&+test_exportempty_pack&&# and then incrementals between each pair of commitslast=&&
@@ -49,6 +56,12 @@ repack_into_n () {last=$revdone<pushes&&+(+findstaging-typef-name'pack-*.pack'|+xargs-n1basename|grep-v"$base_pack"&&+printf"^pack-%s.pack\n"$base_pack+)>stdin.packs+# and install the whole thingrm-f.git/objects/pack/*&&mvstaging/*.git/objects/pack/
@@ -91,6 +104,23 @@ do--reflog--indexed-objects--delta-base-offset\--stdout</dev/null>/dev/null'++test_perf"repack with kept ($nr_packs)"'+gitpack-objects--keep-true-parents\+--keep-pack=pack-$empty_pack.pack\+--honor-pack-keep--non-empty--all\+--reflog--indexed-objects--delta-base-offset\+--stdout</dev/null>/dev/null+'++test_perf"repack with --stdin-packs ($nr_packs)"'+gitpack-objects\+--keep-true-parents\+--stdin-packs\+--non-empty\+--delta-base-offset\+--stdout<stdin.packs>/dev/null+'done# Measure pack loading with 10,000 packs.
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:43
From: Jeff King <redacted>
In a recent patch we added a function 'find_kept_pack_entry()' to look
for an object only among kept packs.
While this function avoids doing any lookup work in non-kept packs, it
is still linear in the number of packs, since we have to traverse the
linked list of packs once per object. Let's cache a reduced version of
that list to save us time.
Note that this cache will last the lifetime of the program. We could
invalidate it on reprepare_packed_git(), but there's not much point in
being rigorous here:
- we might already fail to notice new .keep packs showing up after the
program starts. We only reprepare_packed_git() when we fail to find
an object. But adding a new pack won't cause that to happen.
Somebody repacking could add a new pack and delete an old one, but
most of the time we'd have a descriptor or mmap open to the old
pack anyway, so we might not even notice.
- in pack-objects we already cache the .keep state at startup, since
56dfeb6263 (pack-objects: compute local/ignore_pack_keep early,
2016-07-29). So this is just extending that concept further.
- we don't have to worry about any packed_git being removed; we always
keep the old structs around, even after reprepare_packed_git()
We do defensively invalidate the cache in case the set of kept packs
being asked for changes (e.g., only in-core kept packs were cached, but
suddenly the caller also wants on-disk kept packs, too). In theory we
could build all three caches and switch between them, but it's not
necessary, since this patch (and series) never changes the set of kept
packs that it wants to inspect from the cache.
So that "optimization" is more about being defensive in the face of
future changes than it is about asking for multiple kinds of kept packs
in this patch.
Here are p5303 results (as always, measured against the kernel):
Test HEAD^ HEAD
-----------------------------------------------------------------------------------------------
5303.5: repack (1) 57.34(54.66+10.88) 56.98(54.36+10.98) -0.6%
5303.6: repack with kept (1) 57.38(54.83+10.49) 57.17(54.97+10.26) -0.4%
5303.11: repack (50) 71.70(88.99+4.74) 71.62(88.48+5.08) -0.1%
5303.12: repack with kept (50) 72.58(89.61+4.78) 71.56(88.80+4.59) -1.4%
5303.17: repack (1000) 217.19(491.72+14.25) 217.31(490.82+14.53) +0.1%
5303.18: repack with kept (1000) 246.12(520.07+14.93) 217.08(490.37+15.10) -11.8%
and the --stdin-packs case, which scales a little bit better (although
not by that much even at 1,000 packs):
5303.7: repack with --stdin-packs (1) 0.00(0.00+0.00) 0.00(0.00+0.00) =
5303.13: repack with --stdin-packs (50) 3.43(11.75+0.24) 3.43(11.69+0.30) +0.0%
5303.19: repack with --stdin-packs (1000) 130.50(307.15+7.66) 125.13(301.36+8.04) -4.1%
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
object-store.h | 5 +++
packfile.c | 99 ++++++++++++++++++++++++++++----------------------
2 files changed, 61 insertions(+), 43 deletions(-)
@@ -153,6 +153,11 @@ struct raw_object_store {/* A most-recently-used ordered version of the packed_git list. */structlist_headpacked_git_mru;+struct{+structpacked_git**packs;+unsignedflags;+}kept_pack_cache;+/**Amapofpackfilestopacked_gitstructsfortrackingwhich*packshavebeenloadedalready.
From: Taylor Blau <hidden> Date: 2021-02-18 03:15:46
Often it is useful to both:
- have relatively few packfiles in a repository, and
- avoid having so few packfiles in a repository that we repack its
entire contents regularly
This patch implements a '--geometric=<n>' option in 'git repack'. This
allows the caller to specify that they would like each pack to be at
least a factor times as large as the previous largest pack (by object
count).
Concretely, say that a repository has 'n' packfiles, labeled P1, P2,
..., up to Pn. Each packfile has an object count equal to 'objects(Pn)'.
With a geometric factor of 'r', it should be that:
objects(Pi) > r*objects(P(i-1))
for all i in [1, n], where the packs are sorted by
objects(P1) <= objects(P2) <= ... <= objects(Pn).
Since finding a true optimal repacking is NP-hard, we approximate it
along two directions:
1. We assume that there is a cutoff of packs _before starting the
repack_ where everything to the right of that cut-off already forms
a geometric progression (or no cutoff exists and everything must be
repacked).
2. We assume that everything smaller than the cutoff count must be
repacked. This forms our base assumption, but it can also cause
even the "heavy" packs to get repacked, for e.g., if we have 6
packs containing the following number of objects:
1, 1, 1, 2, 4, 32
then we would place the cutoff between '1, 1' and '1, 2, 4, 32',
rolling up the first two packs into a pack with 2 objects. That
breaks our progression and leaves us:
2, 1, 2, 4, 32
^
(where the '^' indicates the position of our split). To restore a
progression, we move the split forward (towards larger packs)
joining each pack into our new pack until a geometric progression
is restored. Here, that looks like:
2, 1, 2, 4, 32 ~> 3, 2, 4, 32 ~> 5, 4, 32 ~> ... ~> 9, 32
^ ^ ^ ^
This has the advantage of not repacking the heavy-side of packs too
often while also only creating one new pack at a time. Another wrinkle
is that we assume that loose, indexed, and reflog'd objects are
insignificant, and lump them into any new pack that we create. This can
lead to non-idempotent results.
Suggested-by: Derrick Stolee <redacted>
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-repack.txt | 22 +++++
builtin/repack.c | 187 ++++++++++++++++++++++++++++++++++-
t/t7703-repack-geometric.sh | 137 +++++++++++++++++++++++++
3 files changed, 342 insertions(+), 4 deletions(-)
create mode 100755 t/t7703-repack-geometric.sh
@@ -165,6 +165,28 @@ depth is 4095. Pass the `--delta-islands` option to `git-pack-objects`, see linkgit:git-pack-objects[1].+-g=<factor>::+--geometric=<factor>::+ Arrange resulting pack structure so that each successive pack+ contains at least `<factor>` times the number of objects as the+ next-largest pack.+++`git repack` ensures this by determining a "cut" of packfiles that need+to be repacked into one in order to ensure a geometric progression. It+picks the smallest set of packfiles such that as many of the larger+packfiles (by count of objects contained in that pack) may be left+intact.+++Unlike other repack modes, the set of objects to pack is determined+uniquely by the set of packs being "rolled-up"; in other words, the+packs determined to need to be combined in order to restore a geometric+progression.+++Loose objects are implicitly included in this "roll-up", without respect+to their reachability. This is subject to change in the future. This+option (implying a drastically different repack mode) is not guarenteed+to work with all other combinations of option to `git repack`).+ Configuration -------------
@@ -356,6 +476,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)N_("repack objects in packs marked with .keep")),OPT_STRING_LIST(0,"keep-pack",&keep_pack_list,N_("name"),N_("do not repack this pack")),+OPT_INTEGER('g',"geometric",&geometric_factor,+N_("find a geometric progression with factor <N>")),OPT_END()};
@@ -382,6 +504,13 @@ int cmd_repack(int argc, const char **argv, const char *prefix)if(write_bitmaps&&!(pack_everything&ALL_INTO_ONE))die(_(incremental_bitmap_conflict_error));+if(geometric_factor){+if(pack_everything)+die(_("--geometric is incompatible with -A, -a"));+init_pack_geometry(&geometry);+split_pack_geometry(geometry,geometric_factor);+}+packdir=mkpathdup("%s/pack",get_object_directory());packtmp=mkpathdup("%s/.tmp-%d-pack",packdir,(int)getpid());
@@ -0,0 +1,137 @@+#!/bin/sh++test_description='git repack --geometric works correctly'++../test-lib.sh++GIT_TEST_MULTI_PACK_INDEX=0++objdir=.git/objects+midx=$objdir/pack/multi-pack-index++test_expect_success'--geometric with no packs''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++gitrepack--geometric2>out&&+test_i18ngrep"Nothing new to pack"out+)+'++test_expect_success'--geometric with an intact progression''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# These packs already form a geometric progression.+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=22&&# 6 objects+test_commit_bulk--start=44&&# 12 objects++find$objdir/pack-name"*.pack"|sort>expect&&+gitrepack--geometric2-d&&+find$objdir/pack-name"*.pack"|sort>actual&&++test_cmpexpectactual+)+'++test_expect_success'--geometric with small-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+find$objdir/pack-name"*.pack"|sort>small&&+test_commit_bulk--start=34&&# 12 objects+test_commit_bulk--start=78&&# 24 objects+find$objdir/pack-name"*.pack"|sort>before&&++gitrepack--geometric2-d&&++# Three packs in total; two of the existing large ones, and one+# new one.+find$objdir/pack-name"*.pack"|sort>after&&+test_line_count=3after&&+comm-3smallbefore|tr-d"\t">large&&+grep-qFflargeafter+)+'++test_expect_success'--geometric with small- and large-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# size(small1) + size(small2) > size(medium) / 2+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+test_commit_bulk--start=23&&# 7 objects+test_commit_bulk--start=69&&# 27 objects &&++find$objdir/pack-name"*.pack"|sort>before&&++gitrepack--geometric2-d&&++find$objdir/pack-name"*.pack"|sort>after&&+comm-12beforeafter>untouched&&++# Two packs in total; the largest pack from before running "git+# repack", and one new one.+test_line_count=1untouched&&+test_line_count=2after+)+'++test_expect_success'--geometric ignores kept packs''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++test_commitkept&&# 3 objects+test_commitpack&&# 3 objects++KEPT=$(gitpack-objects--revs$objdir/pack/pack<<-EOF+refs/tags/kept+EOF+)&&+PACK=$(gitpack-objects--revs$objdir/pack/pack<<-EOF+refs/tags/pack+^refs/tags/kept+EOF+)&&++# neither pack contains more than twice the number of objects in+# the other, so they should be combined. but, marking one as+# .kept on disk will "freeze" it, so the pack structure should+# remain unchanged.+touch$objdir/pack/pack-$KEPT.keep&&++find$objdir/pack-name"*.pack"|sort>before&&+gitrepack--geometric2-d&&+find$objdir/pack-name"*.pack"|sort>after&&++# both packs should still exist+test_path_is_file$objdir/pack/pack-$KEPT.pack&&+test_path_is_file$objdir/pack/pack-$PACK.pack&&++# and no new packs should be created+test_cmpbeforeafter&&++# Passing --pack-kept-objects causes packs with a .keep file to+# be repacked, too.+gitrepack--geometric2-d--pack-kept-objects&&++find$objdir/pack-name"*.pack">after&&+test_line_count=1after+)+'++test_done
From: Jeff King <hidden> Date: 2021-02-23 00:31:58
On Wed, Feb 17, 2021 at 10:14:11PM -0500, Taylor Blau wrote:
Here is another updated version of mine and Peff's series to add a new 'git
repack --geometric' mode which supports repacking a repository into a geometric
progression of packs by object count.
Thanks. This version looks pretty good to me. I have a few inline
comments below. Mostly just observations, but there a couple tiny nits
that I think may justify one more re-roll.
14: ddc2896caa ! 2: 82f6b45463 revision: learn '--no-kept-objects'
@@ Commit message
certain packs alone (for e.g., when doing a geometric repack that has
some "large" packs which are kept in-core that it wants to leave alone).
+ Note that this option is not guaranteed to produce exactly the set of
+ objects that aren't in kept packs, since it's possible the traversal
+ order may end up in a situation where a non-kept ancestor was "cut off"
+ by a kept object (at which point we would stop traversing). But, we
+ don't care about absolute correctness here, since this will eventually
+ be used as a purely additive guide in an upcoming new repack mode.
+
+ Explicitly avoid documenting this new flag, since it is only used
+ internally. In theory we could avoid even adding it rev-list, but being
+ able to spell this option out on the command-line makes some special
+ cases easier to test without promising to keep it behaving consistently
+ forever. Those tricky cases are exercised in t6114.
We don't have a real procedure for marking something as "off limits" for
users. IMHO omitting it from the documentation and putting an explicit
note in the commit message is probably enough. It would be perhaps
stronger to mark it explicitly as "do not touch" in the documentation,
but then we are polluting the documentation. :)
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
+ die(_("could not find pack '%s'"), item->string);
+ p->pack_keep_in_core = 1;
+ }
++
++ /*
++ * Order packs by ascending mtime; use QSORT directly to access the
++ * string_list_item's ->util pointer, which string_list_sort() does not
++ * provide.
++ */
++ QSORT(include_packs.items, include_packs.nr, pack_mtime_cmp);
++
I wondered briefly if we should accept the order from the caller, and
make it responsible for any sorting. But in other instances, we are
happy to reorder objects internally for the sake of optimization, so it
probably makes sense here.
I also wondered if we could piggy-back on the sorting of packed_git,
which is already in reverse chronological order. But here our primary
structure is the string-list, so we lose that order.
I'm not sure if your sort function is going the right way, though. It
does:
Does that give us the packs in increasing chronological order, but then
decreasing chronological order within the packs themselves?
17: b5081c01b5 ! 5: 181c104a03 p5303: measure time to repack with keep
@@ Metadata
## Commit message ##
p5303: measure time to repack with keep
- This is the same as the regular repack test, except that we mark the
- single base pack as "kept" and use --assume-kept-packs-closed. The
- theory is that this should be faster than the normal repack, because
- we'll have fewer objects to traverse and process.
+ Add two new tests to measure repack performance. Both test split the
s/test split/tests split/, I think.
+ in the 50-pack case, things start to slow down:
+
+ 5303.11: repack (50) 71.54(88.57+4.84)
+ 5303.12: repack with kept (50) 85.12(102.05+4.94)
+
+ and by the time we hit 1,000 packs, things are substantially worse, even
+ though the resulting pack produced is the same:
+
+ 5303.17: repack (1000) 216.87(490.79+14.57)
+ 5303.18: repack with kept (1000) 665.63(938.87+15.76)
OK, that's the kind of horrendous slowdown I knew we could demonstrate. :)
I'm excited to see the numbers improve in the next patch.
+ Likewise, the scaling is pretty extreme on --stdin-packs:
+
+ 5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00)
+ 5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24)
+ 5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Your "that's because" is a little confusing to me. It certainly applies
to the repack vs repack-with-kept comparisons for a given number of
packs. But the scaling on the three --stdin-packs tests is high because
each subsequent test is being asked to do a lot more work. But they're
still cheaper than the matching "repack" case with a given number of
packs. Just not _as_ cheap as they would be if the kept code weren't so
slow.
Would it make sense to reorder those two paragraphs?
The new test itself looks sensible. I like using --keep-pack here to
avoid needing to do any other setup/cleanup. (It does assume that
on-disk and in-core keeps behave the same, but I'm fine with that
white-box assumption, especially for a perf test).
Nice. In each amount we are recovering almost all of the kept slowdown
seen between the repack and repack-with-kept cases. The remaining
slowdown is just from iterating that N-pack linked list, even though we
don't look in any of its .idx files.
+ and the --stdin-packs timings:
+
+ 5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00) 0.00(0.00+0.00) -100.0%
+ 5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24) 3.43(11.75+0.24) -2.8%
+ 5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10) 130.50(307.15+7.66) -33.4%
And of course we see an improvement here, too (as expected, but not as
dramatic because we are doing less work overall).
And now we can see this patch carrying its weight much more than in the
previous iteration of the series. Good. Our N-pack linked list is now a
single element (just the kept pack), so we expect our repack-with-kept
times to match their non-kept partners. And they do.
+ and the --stdin-packs case, which scales a little bit better (although
+ not by that much even at 1,000 packs):
+
+ 5303.7: repack with --stdin-packs (1) 0.00(0.00+0.00) 0.00(0.00+0.00) =
+ 5303.13: repack with --stdin-packs (50) 3.43(11.75+0.24) 3.43(11.69+0.30) +0.0%
+ 5303.19: repack with --stdin-packs (1000) 130.50(307.15+7.66) 125.13(301.36+8.04) -4.1%
And likewise this is less dramatic, but still nice to see.
20: d5561585c2 ! 8: bd492ec142 builtin/repack.c: add '--geometric' option
@@ Documentation/git-repack.txt: depth is 4095.
[...]
++Unlike other repack modes, the set of objects to pack is determined
++uniquely by the set of packs being "rolled-up"; in other words, the
++packs determined to need to be combined in order to restore a geometric
++progression.
And this is the "clarify roll-up" bit I asked for. Looks good.
++Loose objects are implicitly included in this "roll-up", without respect
++to their reachability. This is subject to change in the future. This
++option (implying a drastically different repack mode) is not guarenteed
++to work with all other combinations of option to `git repack`).
Likewise, this is a big improvement. But should it make it clear that
touching loose objects requires --unpacked? I.e., something like:
When `--unpacked` is specified, loose objects are included in this
"roll-up" without respect to their reachability...
Also, s/guarenteed/guaranteed/.
-Peff
From: Taylor Blau <hidden> Date: 2021-02-23 01:07:17
On Mon, Feb 22, 2021 at 07:31:12PM -0500, Jeff King wrote:
On Wed, Feb 17, 2021 at 10:14:11PM -0500, Taylor Blau wrote:
quoted
Here is another updated version of mine and Peff's series to add a new 'git
repack --geometric' mode which supports repacking a repository into a geometric
progression of packs by object count.
Thanks. This version looks pretty good to me. I have a few inline
comments below. Mostly just observations, but there a couple tiny nits
that I think may justify one more re-roll.
Thanks for taking a look; I agree that your comments do justify a
re-roll. But I think that one can be done without touching any of the
code (or maybe one line of code), depending on my question below.
Let's see...
quoted
[snip documentation]
We don't have a real procedure for marking something as "off limits" for
users. IMHO omitting it from the documentation and putting an explicit
note in the commit message is probably enough. It would be perhaps
stronger to mark it explicitly as "do not touch" in the documentation,
but then we are polluting the documentation. :)
I agree; and the second paragraph in the quoted snippet is the "do not
touch" one. So I think this one is good as-is.
I also wondered if we could piggy-back on the sorting of packed_git,
which is already in reverse chronological order. But here our primary
structure is the string-list, so we lose that order.
I'm not sure if your sort function is going the right way, though. It
does:
Does that give us the packs in increasing chronological order, but then
decreasing chronological order within the packs themselves?
I agree we should be sorting and not blindly accepting the order that
the caller gave us, but...
"chronological order within the packs themselves" confuses me. I think
that you mean ordering objects within a pack by their offsets. If so,
then yes: this gives you the oldest pack first (and all of its objects
in their original order), then the second oldest (and all of its
objects) and so on.
Could you clarify a bit how you'd expect to sort the objects in two
packs?
quoted
17: b5081c01b5 ! 5: 181c104a03 p5303: measure time to repack with keep
@@ Metadata
## Commit message ##
p5303: measure time to repack with keep
- This is the same as the regular repack test, except that we mark the
- single base pack as "kept" and use --assume-kept-packs-closed. The
- theory is that this should be faster than the normal repack, because
- we'll have fewer objects to traverse and process.
+ Add two new tests to measure repack performance. Both test split the
s/test split/tests split/, I think.
Good eyes, thanks.
quoted
+ Likewise, the scaling is pretty extreme on --stdin-packs:
+
+ 5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00)
+ 5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24)
+ 5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Your "that's because" is a little confusing to me. It certainly applies
to the repack vs repack-with-kept comparisons for a given number of
packs. But the scaling on the three --stdin-packs tests is high because
each subsequent test is being asked to do a lot more work. But they're
still cheaper than the matching "repack" case with a given number of
packs. Just not _as_ cheap as they would be if the kept code weren't so
slow.
Would it make sense to reorder those two paragraphs?
I think so. I did add a tiny parenthetical after my "Likewise, the
scaling is pretty extreme [...]" to say "(but each subsequent test is
also being asked to do more work)".
quoted
++Loose objects are implicitly included in this "roll-up", without respect
++to their reachability. This is subject to change in the future. This
++option (implying a drastically different repack mode) is not guarenteed
++to work with all other combinations of option to `git repack`).
Likewise, this is a big improvement. But should it make it clear that
touching loose objects requires --unpacked? I.e., something like:
When `--unpacked` is specified, loose objects are included in this
"roll-up" without respect to their reachability...
Also, s/guarenteed/guaranteed/.
Does that give us the packs in increasing chronological order, but then
decreasing chronological order within the packs themselves?
I agree we should be sorting and not blindly accepting the order that
the caller gave us, but...
"chronological order within the packs themselves" confuses me. I think
that you mean ordering objects within a pack by their offsets. If so,
then yes: this gives you the oldest pack first (and all of its objects
in their original order), then the second oldest (and all of its
objects) and so on.
Could you clarify a bit how you'd expect to sort the objects in two
packs?
Yes, by "within the packs themselves" I meant the physical order of
objects within an individual pack (sorted by their offsets, as we'd get
from for_each_object_in_pack). We would generally expect that to be
"newest first" within a given pack (modulo some other heuristics, but we
generally follow traversal order from rev-list).
So if the packs themselves are in oldest-first order, won't that create
a weird discontinuity at the pack boundaries?
E.g., imagine we have a linear sequence of commits A..Z in chronological
order, stored in two packs of equal size. Something like:
tick=1234567890
commit() {
tick=$((tick+10))
export GIT_COMMITTER_DATE="@$tick +0000"
git commit --allow-empty -m $1
}
for i in $(perl -le 'print for A..M'); do commit $i; done
git repack -d
sleep 5
for i in $(perl -le 'print for N..Z'); do commit $i; done
git repack -d
Since "repack -d" will use a traversal to decide which objects to pack,
the two packs will have their commits in reverse chronological order:
M..A and Z..N. You can verify that with:
for idx in $(ls -rt .git/objects/pack/*.idx); do
stat --format='==> %y %n' $idx
git show-index <$idx |
sort -n |
awk '{print $2}' |
git --no-pager log --no-walk=unsorted --stdin --format=%s
done
And if we then ran "git repack -ad" to make a new pack, it would be in
newest-to-oldest Z..A order.
But if instead we concatenate the packs after sorting them in
oldest-first order, we'll end up with a pack that contains M..A, then
Z..N. We instead want newest packs first (and then newest objects within
that pack, which is the pack order), then oldest.
In other words, I think your comparison function should be reversed
(return "1" when a->mtime < b->mtime).
(Of course these orders aren't perfect; in a real pack you'd have
non-commit objects, and we'd tweak the write order to keep delta
families together, etc. But our "best guess" should keep packs and
objects-within-packs consistent in newest-first order).
-Peff
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:00
Future callers will want a function to fill a 'struct pack_entry' for a
given object id but _only_ from its position in any kept pack(s).
In particular, an new 'git repack' mode which ensures the resulting
packs form a geometric progress by object count will mark packs that it
does not want to repack as "kept in-core", and it will want to halt a
reachability traversal as soon as it visits an object in any of the kept
packs. But, it does not want to halt the traversal at non-kept, or
.keep packs.
The obvious alternative is 'find_pack_entry()', but this doesn't quite
suffice since it only returns the first pack it finds, which may or may
not be kept (and the mru cache makes it unpredictable which one you'll
get if there are options).
Short of that, you could walk over all packs looking for the object in
each one, but it scales with the number of packs, which may be
prohibitive.
Introduce 'find_kept_pack_entry()', a function which is like
'find_pack_entry()', but only fills in objects in the kept packs.
Handle packs which have .keep files, as well as in-core kept packs
separately, since certain callers will want to distinguish one from the
other. (Though on-disk and in-core kept packs share the adjective
"kept", it is best to think of the two sets as independent.)
There is a gotcha when looking up objects that are duplicated in kept
and non-kept packs, particularly when the MIDX stores the non-kept
version and the caller asked for kept objects only. This could be
resolved by teaching the MIDX to resolve duplicates by always favoring
the kept pack (if one exists), but this breaks an assumption in existing
MIDXs, and so it would require a format change.
The benefit to changing the MIDX in this way is marginal, so we instead
have a more thorough check here which is explained with a comment.
Callers will be added in subsequent patches.
Co-authored-by: Jeff King [off-list ref]
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
packfile.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-----
packfile.h | 5 +++++
2 files changed, 64 insertions(+), 5 deletions(-)
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:00
Here's a very lightly modified version on v3 of mine and Peff's series
to add a new 'git repack --geometric' mode. Almost nothing has changed
since last time, with the exception of:
- Packs listed over standard input to 'git pack-objects --stdin-packs'
are sorted in descending mtime order (and objects are strung
together in pack order as before) so that objects are laid out
roughly newest-to-oldest in the resulting pack.
- Swapped the order of two paragraphs in patch 5 to make the perf
results clearer.
- Mention '--unpacked' specifically in the documentation for 'git
repack --geometric'.
- Typo fixes.
Range-diff is below. It would be good to start merging this down since
we have a release candidate coming up soon, and I'd rather focus future
reviewer efforts on the multi-pack reverse index and bitmaps series
instead of this one.
Jeff King (4):
p5303: add missing &&-chains
p5303: measure time to repack with keep
builtin/pack-objects.c: rewrite honor-pack-keep logic
packfile: add kept-pack cache for find_kept_pack_entry()
Taylor Blau (4):
packfile: introduce 'find_kept_pack_entry()'
revision: learn '--no-kept-objects'
builtin/pack-objects.c: add '--stdin-packs' option
builtin/repack.c: add '--geometric' option
Documentation/git-pack-objects.txt | 10 +
Documentation/git-repack.txt | 23 ++
builtin/pack-objects.c | 333 ++++++++++++++++++++++++-----
builtin/repack.c | 187 +++++++++++++++-
object-store.h | 5 +
packfile.c | 67 ++++++
packfile.h | 5 +
revision.c | 15 ++
revision.h | 4 +
t/perf/p5303-many-packs.sh | 36 +++-
t/t5300-pack-object.sh | 97 +++++++++
t/t6114-keep-packs.sh | 69 ++++++
t/t7703-repack-geometric.sh | 137 ++++++++++++
13 files changed, 926 insertions(+), 62 deletions(-)
create mode 100755 t/t6114-keep-packs.sh
create mode 100755 t/t7703-repack-geometric.sh
Range-diff against v3:
1: aa94edf39b = 1: bb674e5119 packfile: introduce 'find_kept_pack_entry()'
2: 82f6b45463 = 2: c85a915597 revision: learn '--no-kept-objects'
3: 033e4e3f67 ! 3: 649cf9020b builtin/pack-objects.c: add '--stdin-packs' option
@@ builtin/pack-objects.c: static int git_pack_config(const char *k, const char *v,
+ struct packed_git *a = ((const struct string_list_item*)_a)->util;
+ struct packed_git *b = ((const struct string_list_item*)_b)->util;
+
++ /*
++ * order packs by descending mtime so that objects are laid out
++ * roughly as newest-to-oldest
++ */
+ if (a->mtime < b->mtime)
-+ return -1;
-+ else if (b->mtime < a->mtime)
+ return 1;
++ else if (b->mtime < a->mtime)
++ return -1;
+ else
+ return 0;
+}
4: f9a5faf773 = 4: 6de9f0c52b p5303: add missing &&-chains
5: 181c104a03 ! 5: 94e4f3ee3a p5303: measure time to repack with keep
@@ Metadata
## Commit message ##
p5303: measure time to repack with keep
- Add two new tests to measure repack performance. Both test split the
+ Add two new tests to measure repack performance. Both tests split the
repository into synthetic "pushes", and then leave the remaining objects
in a big base pack.
@@ Commit message
5303.17: repack (1000) 216.87(490.79+14.57)
5303.18: repack with kept (1000) 665.63(938.87+15.76)
- Likewise, the scaling is pretty extreme on --stdin-packs:
-
- 5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00)
- 5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24)
- 5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10)
-
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Our solution to that was to notice that most repos don't have keep
@@ Commit message
single .keep, that part of pack-objects slows down again (even if we
have fewer objects total to look at).
+ Likewise, the scaling is pretty extreme on --stdin-packs (but each
+ subsequent test is also being asked to do more work):
+
+ 5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00)
+ 5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24)
+ 5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10)
+
Signed-off-by: Jeff King [off-list ref]
Signed-off-by: Taylor Blau [off-list ref]
6: 67af143fd1 = 6: a116587fb2 builtin/pack-objects.c: rewrite honor-pack-keep logic
7: e9e04b95e7 = 7: db9f07ec1a packfile: add kept-pack cache for find_kept_pack_entry()
8: bd492ec142 ! 8: 51f57d5da2 builtin/repack.c: add '--geometric' option
@@ Documentation/git-repack.txt: depth is 4095.
+packs determined to need to be combined in order to restore a geometric
+progression.
++
-+Loose objects are implicitly included in this "roll-up", without respect
-+to their reachability. This is subject to change in the future. This
-+option (implying a drastically different repack mode) is not guarenteed
-+to work with all other combinations of option to `git repack`).
++When `--unpacked` is specified, loose objects are implicitly included in
++this "roll-up", without respect to their reachability. This is subject
++to change in the future. This option (implying a drastically different
++repack mode) is not guaranteed to work with all other combinations of
++option to `git repack`).
+
Configuration
-------------
--
2.30.0.667.g81c0cbc6fd
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:00
A future caller will want to be able to perform a reachability traversal
which terminates when visiting an object found in a kept pack. The
closest existing option is '--honor-pack-keep', but this isn't quite
what we want. Instead of halting the traversal midway through, a full
traversal is always performed, and the results are only trimmed
afterwords.
Besides needing to introduce a new flag (since culling results
post-facto can be different than halting the traversal as it's
happening), there is an additional wrinkle handling the distinction
in-core and on-disk kept packs. That is: what kinds of kept pack should
stop the traversal?
Introduce '--no-kept-objects[=<on-disk|in-core>]' to specify which kinds
of kept packs, if any, should stop a traversal. This can be useful for
callers that want to perform a reachability analysis, but want to leave
certain packs alone (for e.g., when doing a geometric repack that has
some "large" packs which are kept in-core that it wants to leave alone).
Note that this option is not guaranteed to produce exactly the set of
objects that aren't in kept packs, since it's possible the traversal
order may end up in a situation where a non-kept ancestor was "cut off"
by a kept object (at which point we would stop traversing). But, we
don't care about absolute correctness here, since this will eventually
be used as a purely additive guide in an upcoming new repack mode.
Explicitly avoid documenting this new flag, since it is only used
internally. In theory we could avoid even adding it rev-list, but being
able to spell this option out on the command-line makes some special
cases easier to test without promising to keep it behaving consistently
forever. Those tricky cases are exercised in t6114.
Signed-off-by: Taylor Blau <redacted>
---
revision.c | 15 ++++++++++
revision.h | 4 +++
t/t6114-keep-packs.sh | 69 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 88 insertions(+)
create mode 100755 t/t6114-keep-packs.sh
@@ -0,0 +1,69 @@+#!/bin/sh++test_description='rev-list with .keep packs'+../test-lib.sh++test_expect_success'setup''+test_commitloose&&+test_commitpacked&&+test_commitkept&&++KEPT_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/kept+^refs/tags/packed+EOF+)&&+MISC_PACK=$(gitpack-objects--revs.git/objects/pack/pack<<-EOF+refs/tags/packed+^refs/tags/loose+EOF+)&&++touch.git/objects/pack/pack-$KEPT_PACK.keep+'++rev_list_objects(){+gitrev-list"$@">out&&+sortout+}++idx_objects(){+gitshow-index<$1>expect-idx&&+cut-d" "-f2<expect-idx|sort+}++test_expect_success'--no-kept-objects excludes trees and blobs in .keep packs''+rev_list_objects--objects--all--no-object-names>kept&&+rev_list_objects--objects--all--no-object-names--no-kept-objects>no-kept&&++idx_objects.git/objects/pack/pack-$KEPT_PACK.idx>expect&&+comm-3keptno-kept>actual&&++test_cmpexpectactual+'++test_expect_success'--no-kept-objects excludes kept non-MIDX object''+test_configcore.multiPackIndextrue&&++# Create a pack with just the commit object in pack, and do not mark it+# as kept (even though it appears in $KEPT_PACK, which does have a .keep+# file).+MIDX_PACK=$(gitpack-objects.git/objects/pack/pack<<-EOF+$(gitrev-parsekept)+EOF+)&&++# Write a MIDX containing all packs, but use the version of the commit+# at "kept" in a non-kept pack by touching $MIDX_PACK.+touch.git/objects/pack/pack-$MIDX_PACK.pack&&+gitmulti-pack-indexwrite&&++rev_list_objects--objects--no-object-names--no-kept-objectsHEAD>actual&&+(+idx_objects.git/objects/pack/pack-$MISC_PACK.idx&&+gitrev-list--objects--no-object-namesrefs/tags/loose+)|sort>expect&&+test_cmpexpectactual+'++test_done
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:21
In an upcoming commit, 'git repack' will want to create a pack comprised
of all of the objects in some packs (the included packs) excluding any
objects in some other packs (the excluded packs).
This caller could iterate those packs themselves and feed the objects it
finds to 'git pack-objects' directly over stdin, but this approach has a
few downsides:
- It requires every caller that wants to drive 'git pack-objects' in
this way to implement pack iteration themselves. This forces the
caller to think about details like what order objects are fed to
pack-objects, which callers would likely rather not do.
- If the set of objects in included packs is large, it requires
sending a lot of data over a pipe, which is inefficient.
- The caller is forced to keep track of the excluded objects, too, and
make sure that it doesn't send any objects that appear in both
included and excluded packs.
But the biggest downside is the lack of a reachability traversal.
Because the caller passes in a list of objects directly, those objects
don't get a namehash assigned to them, which can have a negative impact
on the delta selection process, causing 'git pack-objects' to fail to
find good deltas even when they exist.
The caller could formulate a reachability traversal themselves, but the
only way to drive 'git pack-objects' in this way is to do a full
traversal, and then remove objects in the excluded packs after the
traversal is complete. This can be detrimental to callers who care
about performance, especially in repositories with many objects.
Introduce 'git pack-objects --stdin-packs' which remedies these four
concerns.
'git pack-objects --stdin-packs' expects a list of pack names on stdin,
where 'pack-xyz.pack' denotes that pack as included, and
'^pack-xyz.pack' denotes it as excluded. The resulting pack includes all
objects that are present in at least one included pack, and aren't
present in any excluded pack.
To address the delta selection problem, 'git pack-objects --stdin-packs'
works as follows. First, it assembles a list of objects that it is going
to pack, as above. Then, a reachability traversal is started, whose tips
are any commits mentioned in included packs. Upon visiting an object, we
find its corresponding object_entry in the to_pack list, and set its
namehash parameter appropriately.
To avoid the traversal visiting more objects than it needs to, the
traversal is halted upon encountering an object which can be found in an
excluded pack (by marking the excluded packs as kept in-core, and
passing --no-kept-objects=in-core to the revision machinery).
This can cause the traversal to halt early, for example if an object in
an included pack is an ancestor of ones in excluded packs. But stopping
early is OK, since filling in the namehash fields of objects in the
to_pack list is only additive (i.e., having it helps the delta selection
process, but leaving it blank doesn't impact the correctness of the
resulting pack).
Even still, it is unlikely that this hurts us much in practice, since
the 'git repack --geometric' caller (which is introduced in a later
commit) marks small packs as included, and large ones as excluded.
During ordinary use, the small packs usually represent pushes after a
large repack, and so are unlikely to be ancestors of objects that
already exist in the repository.
(I found it convenient while developing this patch to have 'git
pack-objects' report the number of objects which were visited and got
their namehash fields filled in during traversal. This is also included
in the below patch via trace2 data lines).
Suggested-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-pack-objects.txt | 10 ++
builtin/pack-objects.c | 202 ++++++++++++++++++++++++++++-
t/t5300-pack-object.sh | 97 ++++++++++++++
3 files changed, 307 insertions(+), 2 deletions(-)
@@ -85,6 +85,16 @@ base-name:: reference was included in the resulting packfile. This can be useful to send new tags to native Git clients.+--stdin-packs::+ Read the basenames of packfiles (e.g., `pack-1234abcd.pack`)+ from the standard input, instead of object names or revision+ arguments. The resulting pack contains all objects listed in the+ included packs (those not beginning with `^`), excluding any+ objects listed in the excluded packs (beginning with `^`).+++Incompatible with `--revs`, or options that imply `--revs` (such as+`--all`), with the exception of `--unpacked`, which is compatible.+ --window=<n>:: --depth=<n>:: These two options affect how the objects contained in
@@ -2986,6 +2986,190 @@ static int git_pack_config(const char *k, const char *v, void *cb)returngit_default_config(k,v,cb);}+/* Counters for trace2 output when in --stdin-packs mode. */+staticintstdin_packs_found_nr;+staticintstdin_packs_hints_nr;++staticintadd_object_entry_from_pack(conststructobject_id*oid,+structpacked_git*p,+uint32_tpos,+void*_data)+{+structrev_info*revs=_data;+structobject_infooi=OBJECT_INFO_INIT;+off_tofs;+enumobject_typetype;++display_progress(progress_state,++nr_seen);++if(have_duplicate_entry(oid,0))+return0;++ofs=nth_packed_object_offset(p,pos);+if(!want_object_in_pack(oid,0,&p,&ofs))+return0;++oi.typep=&type;+if(packed_object_info(the_repository,p,ofs,&oi)<0)+die(_("could not get type of object %s in pack %s"),+oid_to_hex(oid),p->pack_name);+elseif(type==OBJ_COMMIT){+/*+*commitsinincludedpacksareusedasstartingpointsforthe+*subsequentrevisionwalk+*/+add_pending_oid(revs,NULL,oid,0);+}++stdin_packs_found_nr++;++create_object_entry(oid,type,0,0,0,p,ofs);++return0;+}++staticvoidshow_commit_pack_hint(structcommit*commit,void*_data)+{+/* nothing to do; commits don't have a namehash */+}++staticvoidshow_object_pack_hint(structobject*object,constchar*name,+void*_data)+{+structobject_entry*oe=packlist_find(&to_pack,&object->oid);+if(!oe)+return;++/*+*Our'to_pack'listwasconstructedbyiteratingallobjectspackedin+*includedpacks,andsodoesn'thaveanon-zerohashfieldthatyou+*wouldtypicallypickupduringareachabilitytraversal.+*+*Makeabest-effortattempttofillinthe->hashand->no_try_delta+*hereusinganowinordertoperhapsimprovethedeltaselection+*process.+*/+oe->hash=pack_name_hash(name);+oe->no_try_delta=name&&no_try_delta(name);++stdin_packs_hints_nr++;+}++staticintpack_mtime_cmp(constvoid*_a,constvoid*_b)+{+structpacked_git*a=((conststructstring_list_item*)_a)->util;+structpacked_git*b=((conststructstring_list_item*)_b)->util;++/*+*orderpacksbydescendingmtimesothatobjectsarelaidout+*roughlyasnewest-to-oldest+*/+if(a->mtime<b->mtime)+return1;+elseif(b->mtime<a->mtime)+return-1;+else+return0;+}++staticvoidread_packs_list_from_stdin(void)+{+structstrbufbuf=STRBUF_INIT;+structstring_listinclude_packs=STRING_LIST_INIT_DUP;+structstring_listexclude_packs=STRING_LIST_INIT_DUP;+structstring_list_item*item=NULL;++structpacked_git*p;+structrev_inforevs;++repo_init_revisions(the_repository,&revs,NULL);+/*+*Usearevisionwalktofillinthenamehashofobjectsintheinclude+*packs.Tosavetime,we'llavoidtraversingthroughobjectsthatare+*inexcludedpacks.+*+*Thatmaycauseustoavoidpopulatingallofthenamehashfieldsof+*allincludedobjects,butourgoalisbest-effort,sincethisisonly+*anoptimizationduringdeltaselection.+*/+revs.no_kept_objects=1;+revs.keep_pack_cache_flags|=IN_CORE_KEEP_PACKS;+revs.blob_objects=1;+revs.tree_objects=1;+revs.tag_objects=1;++while(strbuf_getline(&buf,stdin)!=EOF){+if(!buf.len)+continue;++if(*buf.buf=='^')+string_list_append(&exclude_packs,buf.buf+1);+else+string_list_append(&include_packs,buf.buf);++strbuf_reset(&buf);+}++string_list_sort(&include_packs);+string_list_sort(&exclude_packs);++for(p=get_all_packs(the_repository);p;p=p->next){+constchar*pack_name=pack_basename(p);++item=string_list_lookup(&include_packs,pack_name);+if(!item)+item=string_list_lookup(&exclude_packs,pack_name);++if(item)+item->util=p;+}++/*+*Firsthandlealloftheexcludedpacks,markingthemaskeptin-core+*sothatlatercallstoadd_object_entry()discardsanyobjectsthat+*arealsofoundinexcludedpacks.+*/+for_each_string_list_item(item,&exclude_packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+p->pack_keep_in_core=1;+}++/*+*Orderpacksbyascendingmtime;useQSORTdirectlytoaccessthe+*string_list_item's->utilpointer,whichstring_list_sort()doesnot+*provide.+*/+QSORT(include_packs.items,include_packs.nr,pack_mtime_cmp);++for_each_string_list_item(item,&include_packs){+structpacked_git*p=item->util;+if(!p)+die(_("could not find pack '%s'"),item->string);+for_each_object_in_pack(p,+add_object_entry_from_pack,+&revs,+FOR_EACH_OBJECT_PACK_ORDER);+}++if(prepare_revision_walk(&revs))+die(_("revision walk setup failed"));+traverse_commit_list(&revs,+show_commit_pack_hint,+show_object_pack_hint,+NULL);++trace2_data_intmax("pack-objects",the_repository,"stdin_packs_found",+stdin_packs_found_nr);+trace2_data_intmax("pack-objects",the_repository,"stdin_packs_hints",+stdin_packs_hints_nr);++strbuf_release(&buf);+string_list_clear(&include_packs,0);+string_list_clear(&exclude_packs,0);+}+staticvoidread_object_list_from_stdin(void){charline[GIT_MAX_HEXSZ+1+PATH_MAX+2];
@@ -3539,6 +3724,8 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)OPT_SET_INT_F(0,"indexed-objects",&rev_list_index,N_("include objects referred to by the index"),1,PARSE_OPT_NONEG),+OPT_BOOL(0,"stdin-packs",&stdin_packs,+N_("read packs from stdin")),OPT_BOOL(0,"stdout",&pack_to_stdout,N_("output pack to stdout")),OPT_BOOL(0,"include-tag",&include_tag,
@@ -3690,8 +3877,13 @@ int cmd_pack_objects(int argc, const char **argv, const char *prefix)if(filter_options.choice){if(!pack_to_stdout)die(_("cannot use --filter without --stdout"));+if(stdin_packs)+die(_("cannot use --filter with --stdin-packs"));}+if(stdin_packs&&use_internal_rev_list)+die(_("cannot use internal rev list with --stdin-packs"));+/**"soft"reasonsnottousebitmaps-foron-diskrepackbydefaultwewant*
@@ -532,4 +532,101 @@ test_expect_success 'prefetch objects' 'test_line_count=1donelines'+test_expect_success'setup for --stdin-packs tests''+gitinitstdin-packs&&+(+cdstdin-packs&&++test_commitA&&+test_commitB&&+test_commitC&&++foridinABC+do+gitpack-objects.git/objects/pack/pack-$id\+--incremental--revs<<-EOF+refs/tags/$id+EOF+done&&++ls-la.git/objects/pack+)+'++test_expect_success'--stdin-packs with excluded packs''+(+cdstdin-packs&&++PACK_A="$(basename.git/objects/pack/pack-A-*.pack)"&&+PACK_B="$(basename.git/objects/pack/pack-B-*.pack)"&&+PACK_C="$(basename.git/objects/pack/pack-C-*.pack)"&&++gitpack-objectstest--stdin-packs<<-EOF&&+$PACK_A+^$PACK_B+$PACK_C+EOF++(+gitshow-index<$(ls.git/objects/pack/pack-A-*.idx)&&+gitshow-index<$(ls.git/objects/pack/pack-C-*.idx)+)>expect.raw&&+gitshow-index<$(lstest-*.idx)>actual.raw&&++cut-d" "-f2<expect.raw|sort>expect&&+cut-d" "-f2<actual.raw|sort>actual&&+test_cmpexpectactual+)+'++test_expect_success'--stdin-packs is incompatible with --filter''+(+cdstdin-packs&&+test_must_failgitpack-objects--stdin-packs--stdout\+--filter=blob:none</dev/null2>err&&+test_i18ngrep"cannot use --filter with --stdin-packs"err+)+'++test_expect_success'--stdin-packs is incompatible with --revs''+(+cdstdin-packs&&+test_must_failgitpack-objects--stdin-packs--revsout\+</dev/null2>err&&+test_i18ngrep"cannot use internal rev list with --stdin-packs"err+)+'++test_expect_success'--stdin-packs with loose objects''+(+cdstdin-packs&&++PACK_A="$(basename.git/objects/pack/pack-A-*.pack)"&&+PACK_B="$(basename.git/objects/pack/pack-B-*.pack)"&&+PACK_C="$(basename.git/objects/pack/pack-C-*.pack)"&&++test_commitD&&# loose++gitpack-objectstest2--stdin-packs--unpacked<<-EOF&&+$PACK_A+^$PACK_B+$PACK_C+EOF++(+gitshow-index<$(ls.git/objects/pack/pack-A-*.idx)&&+gitshow-index<$(ls.git/objects/pack/pack-C-*.idx)&&+gitrev-list--objects--no-object-names\+refs/tags/C..refs/tags/D++)>expect.raw&&+ls-la.&&+gitshow-index<$(lstest2-*.idx)>actual.raw&&++cut-d" "-f2<expect.raw|sort>expect&&+cut-d" "-f2<actual.raw|sort>actual&&+test_cmpexpectactual+)+'+ test_done
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:24
From: Jeff King <redacted>
These are in a helper function, so the usual chain-lint doesn't notice
them. This function is still not perfect, as it has some git invocations
on the left-hand-side of the pipe, but it's primary purpose is timing,
not finding bugs or correctness issues.
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
@@ -28,11 +28,11 @@ repack_into_n () {push@commits,$_if$.%5==1;}printreverse@commits;-'"$1">pushes+'"$1">pushes&&# create base packfilehead-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack+gitpack-objects--delta-base-offset--revsstaging/pack&&# and then incrementals between each pair of commitslast=&&
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:26
From: Jeff King <redacted>
Now that we have find_kept_pack_entry(), we don't have to manually keep
hunting through every pack to find a possible "kept" duplicate of the
object. This should be faster, assuming only a portion of your total
packs are actually kept.
Note that we have to re-order the logic a bit here; we can deal with the
disqualifying situations first (e.g., finding the object in a non-local
pack with --local), then "kept" situation(s), and then just fall back to
other "--local" conditions.
Here are the results from p5303 (measurements again taken on the
kernel):
Test HEAD^ HEAD
-----------------------------------------------------------------------------------------------
5303.5: repack (1) 57.26(54.59+10.84) 57.34(54.66+10.88) +0.1%
5303.6: repack with kept (1) 57.33(54.80+10.51) 57.38(54.83+10.49) +0.1%
5303.11: repack (50) 71.54(88.57+4.84) 71.70(88.99+4.74) +0.2%
5303.12: repack with kept (50) 85.12(102.05+4.94) 72.58(89.61+4.78) -14.7%
5303.17: repack (1000) 216.87(490.79+14.57) 217.19(491.72+14.25) +0.1%
5303.18: repack with kept (1000) 665.63(938.87+15.76) 246.12(520.07+14.93) -63.0%
and the --stdin-packs timings:
5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00) 0.00(0.00+0.00) -100.0%
5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24) 3.43(11.75+0.24) -2.8%
5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10) 130.50(307.15+7.66) -33.4%
So our repack with an empty .keep pack is roughly as fast as one without
a .keep pack up to 50 packs. But the --stdin-packs case scales a little
better, too.
Notably, it is faster than a repack of the same size and a kept pack. It
looks at fewer objects, of course, but the penalty for looking at many
packs isn't as costly.
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
builtin/pack-objects.c | 131 ++++++++++++++++++++++++-----------------
1 file changed, 78 insertions(+), 53 deletions(-)
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:28
From: Jeff King <redacted>
Add two new tests to measure repack performance. Both tests split the
repository into synthetic "pushes", and then leave the remaining objects
in a big base pack.
The first new test marks an empty pack as "kept" and then passes
--honor-pack-keep to avoid including objects in it. That doesn't change
the resulting pack, but it does let us compare to the normal repack case
to see how much overhead we add to check whether objects are kept or
not.
The other test is of --stdin-packs, which gives us a sense of how that
number scales based on the number of packs we provide as input. In each
of those tests, the empty pack isn't considered, but the residual pack
(objects that were left over and not included in one of the synthetic
push packs) is marked as kept.
(Note that in the single-pack case of the --stdin-packs test, there is
nothing do since there are no non-excluded packs).
Here are some timings on a recent clone of the kernel:
5303.5: repack (1) 57.26(54.59+10.84)
5303.6: repack with kept (1) 57.33(54.80+10.51)
in the 50-pack case, things start to slow down:
5303.11: repack (50) 71.54(88.57+4.84)
5303.12: repack with kept (50) 85.12(102.05+4.94)
and by the time we hit 1,000 packs, things are substantially worse, even
though the resulting pack produced is the same:
5303.17: repack (1000) 216.87(490.79+14.57)
5303.18: repack with kept (1000) 665.63(938.87+15.76)
That's because the code paths around handling .keep files are known to
scale badly; they look in every single pack file to find each object.
Our solution to that was to notice that most repos don't have keep
files, and to make that case a fast path. But as soon as you add a
single .keep, that part of pack-objects slows down again (even if we
have fewer objects total to look at).
Likewise, the scaling is pretty extreme on --stdin-packs (but each
subsequent test is also being asked to do more work):
5303.7: repack with --stdin-packs (1) 0.01(0.01+0.00)
5303.13: repack with --stdin-packs (50) 3.53(12.07+0.24)
5303.19: repack with --stdin-packs (1000) 195.83(371.82+8.10)
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
t/perf/p5303-many-packs.sh | 34 ++++++++++++++++++++++++++++++++--
1 file changed, 32 insertions(+), 2 deletions(-)
@@ -31,8 +31,15 @@ repack_into_n () {'"$1">pushes&&# create base packfile-head-n1pushes|-gitpack-objects--delta-base-offset--revsstaging/pack&&+base_pack=$(+head-n1pushes|+gitpack-objects--delta-base-offset--revsstaging/pack+)&&+test_exportbase_pack&&++# create an empty packfile+empty_pack=$(gitpack-objectsstaging/pack</dev/null)&&+test_exportempty_pack&&# and then incrementals between each pair of commitslast=&&
@@ -49,6 +56,12 @@ repack_into_n () {last=$revdone<pushes&&+(+findstaging-typef-name'pack-*.pack'|+xargs-n1basename|grep-v"$base_pack"&&+printf"^pack-%s.pack\n"$base_pack+)>stdin.packs+# and install the whole thingrm-f.git/objects/pack/*&&mvstaging/*.git/objects/pack/
@@ -91,6 +104,23 @@ do--reflog--indexed-objects--delta-base-offset\--stdout</dev/null>/dev/null'++test_perf"repack with kept ($nr_packs)"'+gitpack-objects--keep-true-parents\+--keep-pack=pack-$empty_pack.pack\+--honor-pack-keep--non-empty--all\+--reflog--indexed-objects--delta-base-offset\+--stdout</dev/null>/dev/null+'++test_perf"repack with --stdin-packs ($nr_packs)"'+gitpack-objects\+--keep-true-parents\+--stdin-packs\+--non-empty\+--delta-base-offset\+--stdout<stdin.packs>/dev/null+'done# Measure pack loading with 10,000 packs.
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:38
Often it is useful to both:
- have relatively few packfiles in a repository, and
- avoid having so few packfiles in a repository that we repack its
entire contents regularly
This patch implements a '--geometric=<n>' option in 'git repack'. This
allows the caller to specify that they would like each pack to be at
least a factor times as large as the previous largest pack (by object
count).
Concretely, say that a repository has 'n' packfiles, labeled P1, P2,
..., up to Pn. Each packfile has an object count equal to 'objects(Pn)'.
With a geometric factor of 'r', it should be that:
objects(Pi) > r*objects(P(i-1))
for all i in [1, n], where the packs are sorted by
objects(P1) <= objects(P2) <= ... <= objects(Pn).
Since finding a true optimal repacking is NP-hard, we approximate it
along two directions:
1. We assume that there is a cutoff of packs _before starting the
repack_ where everything to the right of that cut-off already forms
a geometric progression (or no cutoff exists and everything must be
repacked).
2. We assume that everything smaller than the cutoff count must be
repacked. This forms our base assumption, but it can also cause
even the "heavy" packs to get repacked, for e.g., if we have 6
packs containing the following number of objects:
1, 1, 1, 2, 4, 32
then we would place the cutoff between '1, 1' and '1, 2, 4, 32',
rolling up the first two packs into a pack with 2 objects. That
breaks our progression and leaves us:
2, 1, 2, 4, 32
^
(where the '^' indicates the position of our split). To restore a
progression, we move the split forward (towards larger packs)
joining each pack into our new pack until a geometric progression
is restored. Here, that looks like:
2, 1, 2, 4, 32 ~> 3, 2, 4, 32 ~> 5, 4, 32 ~> ... ~> 9, 32
^ ^ ^ ^
This has the advantage of not repacking the heavy-side of packs too
often while also only creating one new pack at a time. Another wrinkle
is that we assume that loose, indexed, and reflog'd objects are
insignificant, and lump them into any new pack that we create. This can
lead to non-idempotent results.
Suggested-by: Derrick Stolee <redacted>
Signed-off-by: Taylor Blau <redacted>
---
Documentation/git-repack.txt | 23 +++++
builtin/repack.c | 187 ++++++++++++++++++++++++++++++++++-
t/t7703-repack-geometric.sh | 137 +++++++++++++++++++++++++
3 files changed, 343 insertions(+), 4 deletions(-)
create mode 100755 t/t7703-repack-geometric.sh
@@ -165,6 +165,29 @@ depth is 4095. Pass the `--delta-islands` option to `git-pack-objects`, see linkgit:git-pack-objects[1].+-g=<factor>::+--geometric=<factor>::+ Arrange resulting pack structure so that each successive pack+ contains at least `<factor>` times the number of objects as the+ next-largest pack.+++`git repack` ensures this by determining a "cut" of packfiles that need+to be repacked into one in order to ensure a geometric progression. It+picks the smallest set of packfiles such that as many of the larger+packfiles (by count of objects contained in that pack) may be left+intact.+++Unlike other repack modes, the set of objects to pack is determined+uniquely by the set of packs being "rolled-up"; in other words, the+packs determined to need to be combined in order to restore a geometric+progression.+++When `--unpacked` is specified, loose objects are implicitly included in+this "roll-up", without respect to their reachability. This is subject+to change in the future. This option (implying a drastically different+repack mode) is not guaranteed to work with all other combinations of+option to `git repack`).+ Configuration -------------
@@ -356,6 +476,8 @@ int cmd_repack(int argc, const char **argv, const char *prefix)N_("repack objects in packs marked with .keep")),OPT_STRING_LIST(0,"keep-pack",&keep_pack_list,N_("name"),N_("do not repack this pack")),+OPT_INTEGER('g',"geometric",&geometric_factor,+N_("find a geometric progression with factor <N>")),OPT_END()};
@@ -382,6 +504,13 @@ int cmd_repack(int argc, const char **argv, const char *prefix)if(write_bitmaps&&!(pack_everything&ALL_INTO_ONE))die(_(incremental_bitmap_conflict_error));+if(geometric_factor){+if(pack_everything)+die(_("--geometric is incompatible with -A, -a"));+init_pack_geometry(&geometry);+split_pack_geometry(geometry,geometric_factor);+}+packdir=mkpathdup("%s/pack",get_object_directory());packtmp=mkpathdup("%s/.tmp-%d-pack",packdir,(int)getpid());
@@ -0,0 +1,137 @@+#!/bin/sh++test_description='git repack --geometric works correctly'++../test-lib.sh++GIT_TEST_MULTI_PACK_INDEX=0++objdir=.git/objects+midx=$objdir/pack/multi-pack-index++test_expect_success'--geometric with no packs''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++gitrepack--geometric2>out&&+test_i18ngrep"Nothing new to pack"out+)+'++test_expect_success'--geometric with an intact progression''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# These packs already form a geometric progression.+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=22&&# 6 objects+test_commit_bulk--start=44&&# 12 objects++find$objdir/pack-name"*.pack"|sort>expect&&+gitrepack--geometric2-d&&+find$objdir/pack-name"*.pack"|sort>actual&&++test_cmpexpectactual+)+'++test_expect_success'--geometric with small-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+find$objdir/pack-name"*.pack"|sort>small&&+test_commit_bulk--start=34&&# 12 objects+test_commit_bulk--start=78&&# 24 objects+find$objdir/pack-name"*.pack"|sort>before&&++gitrepack--geometric2-d&&++# Three packs in total; two of the existing large ones, and one+# new one.+find$objdir/pack-name"*.pack"|sort>after&&+test_line_count=3after&&+comm-3smallbefore|tr-d"\t">large&&+grep-qFflargeafter+)+'++test_expect_success'--geometric with small- and large-pack rollup''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++# size(small1) + size(small2) > size(medium) / 2+test_commit_bulk--start=11&&# 3 objects+test_commit_bulk--start=21&&# 3 objects+test_commit_bulk--start=23&&# 7 objects+test_commit_bulk--start=69&&# 27 objects &&++find$objdir/pack-name"*.pack"|sort>before&&++gitrepack--geometric2-d&&++find$objdir/pack-name"*.pack"|sort>after&&+comm-12beforeafter>untouched&&++# Two packs in total; the largest pack from before running "git+# repack", and one new one.+test_line_count=1untouched&&+test_line_count=2after+)+'++test_expect_success'--geometric ignores kept packs''+gitinitgeometric&&+test_when_finished"rm -fr geometric"&&+(+cdgeometric&&++test_commitkept&&# 3 objects+test_commitpack&&# 3 objects++KEPT=$(gitpack-objects--revs$objdir/pack/pack<<-EOF+refs/tags/kept+EOF+)&&+PACK=$(gitpack-objects--revs$objdir/pack/pack<<-EOF+refs/tags/pack+^refs/tags/kept+EOF+)&&++# neither pack contains more than twice the number of objects in+# the other, so they should be combined. but, marking one as+# .kept on disk will "freeze" it, so the pack structure should+# remain unchanged.+touch$objdir/pack/pack-$KEPT.keep&&++find$objdir/pack-name"*.pack"|sort>before&&+gitrepack--geometric2-d&&+find$objdir/pack-name"*.pack"|sort>after&&++# both packs should still exist+test_path_is_file$objdir/pack/pack-$KEPT.pack&&+test_path_is_file$objdir/pack/pack-$PACK.pack&&++# and no new packs should be created+test_cmpbeforeafter&&++# Passing --pack-kept-objects causes packs with a .keep file to+# be repacked, too.+gitrepack--geometric2-d--pack-kept-objects&&++find$objdir/pack-name"*.pack">after&&+test_line_count=1after+)+'++test_done
From: Taylor Blau <hidden> Date: 2021-02-23 02:26:40
From: Jeff King <redacted>
In a recent patch we added a function 'find_kept_pack_entry()' to look
for an object only among kept packs.
While this function avoids doing any lookup work in non-kept packs, it
is still linear in the number of packs, since we have to traverse the
linked list of packs once per object. Let's cache a reduced version of
that list to save us time.
Note that this cache will last the lifetime of the program. We could
invalidate it on reprepare_packed_git(), but there's not much point in
being rigorous here:
- we might already fail to notice new .keep packs showing up after the
program starts. We only reprepare_packed_git() when we fail to find
an object. But adding a new pack won't cause that to happen.
Somebody repacking could add a new pack and delete an old one, but
most of the time we'd have a descriptor or mmap open to the old
pack anyway, so we might not even notice.
- in pack-objects we already cache the .keep state at startup, since
56dfeb6263 (pack-objects: compute local/ignore_pack_keep early,
2016-07-29). So this is just extending that concept further.
- we don't have to worry about any packed_git being removed; we always
keep the old structs around, even after reprepare_packed_git()
We do defensively invalidate the cache in case the set of kept packs
being asked for changes (e.g., only in-core kept packs were cached, but
suddenly the caller also wants on-disk kept packs, too). In theory we
could build all three caches and switch between them, but it's not
necessary, since this patch (and series) never changes the set of kept
packs that it wants to inspect from the cache.
So that "optimization" is more about being defensive in the face of
future changes than it is about asking for multiple kinds of kept packs
in this patch.
Here are p5303 results (as always, measured against the kernel):
Test HEAD^ HEAD
-----------------------------------------------------------------------------------------------
5303.5: repack (1) 57.34(54.66+10.88) 56.98(54.36+10.98) -0.6%
5303.6: repack with kept (1) 57.38(54.83+10.49) 57.17(54.97+10.26) -0.4%
5303.11: repack (50) 71.70(88.99+4.74) 71.62(88.48+5.08) -0.1%
5303.12: repack with kept (50) 72.58(89.61+4.78) 71.56(88.80+4.59) -1.4%
5303.17: repack (1000) 217.19(491.72+14.25) 217.31(490.82+14.53) +0.1%
5303.18: repack with kept (1000) 246.12(520.07+14.93) 217.08(490.37+15.10) -11.8%
and the --stdin-packs case, which scales a little bit better (although
not by that much even at 1,000 packs):
5303.7: repack with --stdin-packs (1) 0.00(0.00+0.00) 0.00(0.00+0.00) =
5303.13: repack with --stdin-packs (50) 3.43(11.75+0.24) 3.43(11.69+0.30) +0.0%
5303.19: repack with --stdin-packs (1000) 130.50(307.15+7.66) 125.13(301.36+8.04) -4.1%
Signed-off-by: Jeff King <redacted>
Signed-off-by: Taylor Blau <redacted>
---
object-store.h | 5 +++
packfile.c | 99 ++++++++++++++++++++++++++++----------------------
2 files changed, 61 insertions(+), 43 deletions(-)
@@ -153,6 +153,11 @@ struct raw_object_store {/* A most-recently-used ordered version of the packed_git list. */structlist_headpacked_git_mru;+struct{+structpacked_git**packs;+unsignedflags;+}kept_pack_cache;+/**Amapofpackfilestopacked_gitstructsfortrackingwhich*packshavebeenloadedalready.
From: Jeff King <hidden> Date: 2021-02-23 03:40:50
On Mon, Feb 22, 2021 at 09:24:59PM -0500, Taylor Blau wrote:
Here's a very lightly modified version on v3 of mine and Peff's series
to add a new 'git repack --geometric' mode. Almost nothing has changed
since last time, with the exception of:
- Packs listed over standard input to 'git pack-objects --stdin-packs'
are sorted in descending mtime order (and objects are strung
together in pack order as before) so that objects are laid out
roughly newest-to-oldest in the resulting pack.
- Swapped the order of two paragraphs in patch 5 to make the perf
results clearer.
- Mention '--unpacked' specifically in the documentation for 'git
repack --geometric'.
- Typo fixes.